A discord bot for teal.fm
discord tealfm music

Compare changes

Choose any two refs to compare.

+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
··· 13 }, 14 "dependencies": { 15 "@tealfmbot/common": "workspace:*", 16 "discord.js": "^14.25.1" 17 }, 18 "devDependencies": {
··· 13 }, 14 "dependencies": { 15 "@tealfmbot/common": "workspace:*", 16 + "@tealfmbot/database": "workspace:*", 17 "discord.js": "^14.25.1" 18 }, 19 "devDependencies": {
+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
··· 10 }, 11 "dependencies": { 12 "@atproto/tap": "^0.0.2", 13 - "@tealfmbot/common": "workspace:*" 14 }, 15 "devDependencies": { 16 "@tealfmbot/tsconfig": "workspace:*",
··· 10 }, 11 "dependencies": { 12 "@atproto/tap": "^0.0.2", 13 + "@tealfmbot/common": "workspace:*", 14 + "@tealfmbot/database": "workspace:*" 15 }, 16 "devDependencies": { 17 "@tealfmbot/tsconfig": "workspace:*",
+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
··· 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
··· 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
···
··· 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
··· 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
···
··· 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
···
··· 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
··· 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:*