+17
apps/bot/commands/recent.ts
+17
apps/bot/commands/recent.ts
···
···
1
+
import { logger } from "@tealfmbot/common/logger";
2
+
// import {db} from "@tealfmbot/database/db"
3
+
import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
4
+
5
+
export default {
6
+
data: new SlashCommandBuilder()
7
+
.setName("recent")
8
+
.setDescription(
9
+
"Show your most recently played track",
10
+
),
11
+
async execute(interaction: ChatInputCommandInteraction) {
12
+
await interaction.reply("recent");
13
+
logger.info(
14
+
`fetching recent track for ${interaction.user.username} in guild ${interaction.guildId} at ${new Date().toJSON()}`,
15
+
);
16
+
},
17
+
};
+1
apps/bot/package.json
+1
apps/bot/package.json
+20
-8
apps/tapper/index.ts
+20
-8
apps/tapper/index.ts
···
1
import { SimpleIndexer, Tap } from "@atproto/tap";
2
import { env } from "@tealfmbot/common/constants";
3
-
// import { db } from "./kysely/db.ts"
4
5
const tap = new Tap("https://tap.xero.systems", {
6
adminPassword: env.TAP_ADMIN_PASSWORD,
···
10
11
indexer.record(async (evt, opts) => {
12
const uri = `at://${evt.did}/${evt.collection}/${evt.rkey}`;
13
-
if (evt.action === "create" || evt.action === "update") {
14
-
// await db.insertInto("plays").values({
15
-
// played_time: evt?.record?.playedTime,
16
-
// release_name: evt?.record?.releaseName,
17
-
// track_name: evt?.record?.trackName,
18
-
// user_id: 4
19
-
// }).execute()
20
console.log(evt.record);
21
} else {
22
console.log(`deleted: ${uri}`);
···
1
import { SimpleIndexer, Tap } from "@atproto/tap";
2
import { env } from "@tealfmbot/common/constants";
3
+
import { db } from "@tealfmbot/database/db";
4
+
5
+
import { isTealRecord } from "./utils";
6
7
const tap = new Tap("https://tap.xero.systems", {
8
adminPassword: env.TAP_ADMIN_PASSWORD,
···
12
13
indexer.record(async (evt, opts) => {
14
const uri = `at://${evt.did}/${evt.collection}/${evt.rkey}`;
15
+
if (evt.action === "create") {
16
+
if (isTealRecord(evt.record)) {
17
+
await db
18
+
.insertInto("plays")
19
+
.values({
20
+
cid: evt?.cid,
21
+
rkey: evt?.rkey,
22
+
uri,
23
+
release_name: evt?.record?.releaseName,
24
+
played_time: evt?.record?.playedTime,
25
+
track_name: evt?.record?.trackName,
26
+
indexed_at: new Date().toJSON(),
27
+
live: evt.live,
28
+
user_id: 1,
29
+
})
30
+
.execute();
31
+
}
32
console.log(evt.record);
33
} else {
34
console.log(`deleted: ${uri}`);
+2
-1
apps/tapper/package.json
+2
-1
apps/tapper/package.json
+24
apps/tapper/utils.ts
+24
apps/tapper/utils.ts
···
···
1
+
type TealRecord = {
2
+
$type: "fm.teal.alpha.feed.play";
3
+
trackName: string;
4
+
trackMbId?: string;
5
+
recordingMbId?: string;
6
+
duration?: number;
7
+
releaseName?: string;
8
+
releaseMbId?: string;
9
+
isrc?: string;
10
+
originUrl?: string;
11
+
musicServiceBaseDomain?: string;
12
+
submissionClientAgent?: string;
13
+
playedTime?: Date;
14
+
artists: Artist[];
15
+
};
16
+
17
+
type Artist = {
18
+
artistMbId?: string;
19
+
artistName?: string;
20
+
};
21
+
22
+
export function isTealRecord(record: unknown): record is TealRecord {
23
+
return (record as TealRecord).$type === "fm.teal.alpha.feed.play";
24
+
}
+7
-5
build-and-publish-images.sh
+7
-5
build-and-publish-images.sh
···
1
SHA=$(git rev-parse HEAD)
2
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
3
VERSION=$(git describe --tags --abbrev=0)
4
REGISTRY=atcr.io/besaid.zone
5
-
DID=did:plc:qttsv4e7pu2jl3ilanfgc3zn
6
7
services=(
8
web
···
10
tapper
11
)
12
13
-
echo "building container versions: ${VERSION#v}"
14
15
for svc in ${services[@]}; do
16
docker buildx build \
17
-t $REGISTRY/discostu$svc:${VERSION#v} \
18
--target $svc \
19
--build-arg VERSION=${VERSION#v} \
20
--build-arg SHA=$SHA \
21
-
--build-arg DID=$DID \
22
--build-arg BUILD_DATE=$BUILD_DATE \
23
--pull \
24
-
--no-cache \
25
-
--push .
26
done
···
1
SHA=$(git rev-parse HEAD)
2
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
3
+
LAST_BUILT=$(date -u +%Y%m%d)
4
VERSION=$(git describe --tags --abbrev=0)
5
REGISTRY=atcr.io/besaid.zone
6
7
services=(
8
web
···
10
tapper
11
)
12
13
+
echo "building versioned containers with version ${VERSION#v} and tagging :latest"
14
15
for svc in ${services[@]}; do
16
docker buildx build \
17
-t $REGISTRY/discostu$svc:${VERSION#v} \
18
+
-t $REGISTRY/discostu$svc:latest \
19
+
--platform linux/amd64,linux/arm64 \
20
--target $svc \
21
--build-arg VERSION=${VERSION#v} \
22
--build-arg SHA=$SHA \
23
+
--build-arg DID=did:plc:qttsv4e7pu2jl3ilanfgc3zn \
24
--build-arg BUILD_DATE=$BUILD_DATE \
25
--pull \
26
+
--no-cache .
27
+
# --push .
28
done
+4
-3
docker-compose.prod.yml
+4
-3
docker-compose.prod.yml
···
1
services:
2
web:
3
container_name: web
4
restart: always
5
build:
6
context: .
7
-
dockerfile: Dockerfile
8
target: web
9
ports:
10
- 8002:8002
···
29
container_name: tapper
30
build:
31
context: .
32
-
dockerfile: Dockerfile
33
target: tapper
34
35
depends_on:
···
44
restart: always
45
build:
46
context: .
47
-
dockerfile: Dockerfile
48
target: bot
49
50
depends_on:
···
1
+
name: "Disco Stu Compose - Prod"
2
services:
3
web:
4
container_name: web
5
restart: always
6
build:
7
context: .
8
+
dockerfile: atcr.io/besaid.zone/discostuweb:1.0
9
target: web
10
ports:
11
- 8002:8002
···
30
container_name: tapper
31
build:
32
context: .
33
+
dockerfile: atcr.io/besaid.zone/discostutapper:1.0
34
target: tapper
35
36
depends_on:
···
45
restart: always
46
build:
47
context: .
48
+
dockerfile: atcr.io/besaid.zone/discostubot:1.0
49
target: bot
50
51
depends_on:
+53
justfile
+53
justfile
···
···
1
+
sha := `git rev-parse HEAD`
2
+
build_date := `date -u +%Y-%m-%dT%H:%M:%SZ`
3
+
registry := "atcr.io/besaid.zone"
4
+
5
+
default:
6
+
@just --list
7
+
8
+
release:
9
+
#!/usr/bin/env bash
10
+
VERSION=$(git describe --tags --abbrev=0)
11
+
services=(
12
+
web
13
+
tapper
14
+
bot
15
+
)
16
+
17
+
echo "building versioned containers with version ${VERSION#v} and tagging :latest"
18
+
19
+
for svc in ${services[@]}; do
20
+
docker buildx build \
21
+
-t "{{registry}}"/discostu$svc:${VERSION#v} \
22
+
-t "{{registry}}"/discostu$svc:latest \
23
+
--target $svc \
24
+
--build-arg VERSION=${VERSION#v} \
25
+
--build-arg SHA="{{sha}}" \
26
+
--build-arg DID=did:plc:qttsv4e7pu2jl3ilanfgc3zn \
27
+
--build-arg BUILD_DATE="{{build_date}}" \
28
+
--pull \
29
+
--no-cache \
30
+
--push .
31
+
done
32
+
33
+
latest:
34
+
#!/usr/bin/env bash
35
+
VERSION=$(git describe --tags --abbrev=0)
36
+
services=(
37
+
web
38
+
tapper
39
+
bot
40
+
)
41
+
42
+
for svc in ${services[@]}; do
43
+
docker buildx build \
44
+
-t "{{registry}}"/discostu$svc:latest \
45
+
--target $svc \
46
+
--build-arg VERSION=${VERSION#v} \
47
+
--build-arg SHA="{{sha}}" \
48
+
--build-arg DID=did:plc:qttsv4e7pu2jl3ilanfgc3zn \
49
+
--build-arg BUILD_DATE="{{build_date}}" \
50
+
--pull \
51
+
--no-cache \
52
+
--push .
53
+
done
+9
-5
packages/database/database.d.ts
+9
-5
packages/database/database.d.ts
···
5
6
import type { ColumnType } from "kysely";
7
8
-
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
9
-
? ColumnType<S, I | undefined, U>
10
-
: ColumnType<T, T | undefined, T>;
11
12
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
13
···
27
}
28
29
export interface Plays {
30
id: Generated<number>;
31
indexed_at: Generated<Timestamp>;
32
live: boolean | null;
33
-
played_time: Timestamp;
34
-
release_name: string;
35
track_name: string;
36
user_id: number;
37
}
38
···
5
6
import type { ColumnType } from "kysely";
7
8
+
export type Generated<T> =
9
+
T extends ColumnType<infer S, infer I, infer U>
10
+
? ColumnType<S, I | undefined, U>
11
+
: ColumnType<T, T | undefined, T>;
12
13
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
14
···
28
}
29
30
export interface Plays {
31
+
cid: string | null;
32
id: Generated<number>;
33
indexed_at: Generated<Timestamp>;
34
live: boolean | null;
35
+
played_time: Timestamp | null;
36
+
release_name: string | null;
37
+
rkey: string | null;
38
track_name: string;
39
+
uri: string | null;
40
user_id: number;
41
}
42
+17
packages/database/migrations/1767553973566_add_cid_uri_rkey.ts
+17
packages/database/migrations/1767553973566_add_cid_uri_rkey.ts
···
···
1
+
import type { Kysely } from 'kysely'
2
+
3
+
// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface.
4
+
export async function up(db: Kysely<any>): Promise<void> {
5
+
// up migration code goes here...
6
+
// note: up migrations are mandatory. you must implement this function.
7
+
// For more info, see: https://kysely.dev/docs/migrations
8
+
await db.schema.alterTable("plays").addColumn("cid", "varchar").addColumn("uri", "varchar").addColumn("rkey", "varchar").execute()
9
+
}
10
+
11
+
// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface.
12
+
export async function down(db: Kysely<any>): Promise<void> {
13
+
// down migration code goes here...
14
+
// note: down migrations are optional. you can safely delete this function.
15
+
// For more info, see: https://kysely.dev/docs/migrations
16
+
await db.schema.alterTable("plays").dropColumn("cid").dropColumn("uri").dropColumn("rkey").execute()
17
+
}
+18
packages/database/migrations/1767554974033_release_name_and_played_time_optional.ts
+18
packages/database/migrations/1767554974033_release_name_and_played_time_optional.ts
···
···
1
+
import type { Kysely } from 'kysely'
2
+
3
+
// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface.
4
+
export async function up(db: Kysely<any>): Promise<void> {
5
+
// up migration code goes here...
6
+
// note: up migrations are mandatory. you must implement this function.
7
+
// For more info, see: https://kysely.dev/docs/migrations
8
+
await db.schema.alterTable("plays").alterColumn("release_name", (col) => col.dropNotNull()).alterColumn("played_time", (col) => col.dropNotNull()).execute()
9
+
}
10
+
11
+
// `any` is required here since migrations should be frozen in time. alternatively, keep a "snapshot" db interface.
12
+
export async function down(db: Kysely<any>): Promise<void> {
13
+
// down migration code goes here...
14
+
// note: down migrations are optional. you can safely delete this function.
15
+
// For more info, see: https://kysely.dev/docs/migrations
16
+
await db.schema.alterTable("plays").alterColumn("played_time", (col) => col.setNotNull())
17
+
.alterColumn("release_name", (col) => col.setNotNull()).execute()
18
+
}
+6
pnpm-lock.yaml
+6
pnpm-lock.yaml
···
30
'@tealfmbot/common':
31
specifier: workspace:*
32
version: link:../../packages/common
33
discord.js:
34
specifier: ^14.25.1
35
version: 14.25.1
···
55
'@tealfmbot/common':
56
specifier: workspace:*
57
version: link:../../packages/common
58
devDependencies:
59
'@tealfmbot/tsconfig':
60
specifier: workspace:*
···
30
'@tealfmbot/common':
31
specifier: workspace:*
32
version: link:../../packages/common
33
+
'@tealfmbot/database':
34
+
specifier: workspace:*
35
+
version: link:../../packages/database
36
discord.js:
37
specifier: ^14.25.1
38
version: 14.25.1
···
58
'@tealfmbot/common':
59
specifier: workspace:*
60
version: link:../../packages/common
61
+
'@tealfmbot/database':
62
+
specifier: workspace:*
63
+
version: link:../../packages/database
64
devDependencies:
65
'@tealfmbot/tsconfig':
66
specifier: workspace:*