A discord bot for teal.fm
discord tealfm music

add pino-http

besaid.zone 83d97e11 1c4ba171

verified
+1 -1
.oxfmtrc.json
··· 1 { 2 "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 - "ignorePatterns": ["*.json", "pnpm-lock.yml"], 4 "experimentalSortImports": { 5 "order": "asc" 6 }
··· 1 { 2 "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 + "ignorePatterns": ["*.json", "pnpm-lock.yaml"], 4 "experimentalSortImports": { 5 "order": "asc" 6 }
+6 -2
PLAN.md
··· 1 # rough notes on how I think this should work 2 3 - we start of with no accounts 4 - - `/teal auth` sends user a link to log in with atproto account 5 - after auth success, we take did and send http request to tap instance to start backfilling for repo 6 - user can now use bot commands 7 8 ## planned commands 9 10 - - `teal auth` sends user a link to log in with atproto account 11 - `top <artist>`: top 10 listeners for artist (total amount of plays across all songs / albums) 12 - `recent`: most recent play 13 14 ## web interface for account management...maybe..? 15 16 - link/unlink account (unlinking would basically delete an account and all their plays)
··· 1 # rough notes on how I think this should work 2 3 - we start of with no accounts 4 + - `/auth <did or handle>` sends user a link to log in with atproto account 5 - after auth success, we take did and send http request to tap instance to start backfilling for repo 6 - user can now use bot commands 7 8 ## planned commands 9 10 + - `auth <did or handle>` sends user a link to log in with atproto account 11 - `top <artist>`: top 10 listeners for artist (total amount of plays across all songs / albums) 12 - `recent`: most recent play 13 14 ## web interface for account management...maybe..? 15 16 - link/unlink account (unlinking would basically delete an account and all their plays) 17 + 18 + ## authentication flow 19 + 20 + 1.
+13 -7
apps/bot/commands/auth.ts
··· 1 - import { CommandInteraction, SlashCommandBuilder } from "discord.js"; 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 data: new SlashCommandBuilder() 6 .setName("auth") 7 - .setDescription( 8 - "Authenticate your account with the teal.fm bot to start tracking your listens", 9 - ), 10 - async execute(interaction: CommandInteraction) { 11 - await interaction.reply("placeholder"); 12 - logger.info("auth command sent"); 13 }, 14 };
··· 1 import { logger } from "@tealfmbot/common/logger.ts"; 2 + import { ChatInputCommandInteraction, InteractionContextType, SlashCommandBuilder } from "discord.js"; 3 4 export default { 5 data: new SlashCommandBuilder() 6 .setName("auth") 7 + .setDescription("Authenticate your account with the teal.fm bot to start tracking your listens") 8 + .addStringOption((option) => 9 + option 10 + .setName("identifier") 11 + .setDescription("e.g. 'handle.bsky.social or did:plc...'") 12 + .setRequired(true) 13 + .setMinLength(1), 14 + ).setContexts(InteractionContextType.Guild), 15 + async execute(interaction: ChatInputCommandInteraction) { 16 + const identifier = interaction.options.getString("identifier") 17 + await interaction.reply(`hello ${identifier}`) 18 + logger.info(`starting authentication process for ${identifier} at ${new Date().toJSON()}`); 19 }, 20 };
+1 -1
apps/bot/commands/ping.ts
··· 1 - import { CommandInteraction, SlashCommandBuilder } from "discord.js"; 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 data: new SlashCommandBuilder().setName("ping").setDescription("replies with pong"),
··· 1 import { logger } from "@tealfmbot/common/logger.ts"; 2 + import { CommandInteraction, SlashCommandBuilder } from "discord.js"; 3 4 export default { 5 data: new SlashCommandBuilder().setName("ping").setDescription("replies with pong"),
+1 -1
apps/bot/commands/top.ts
··· 1 - import { CommandInteraction, SlashCommandBuilder } from "discord.js"; 2 import { logger } from "@tealfmbot/common/logger.ts"; 3 4 export default { 5 data: new SlashCommandBuilder()
··· 1 import { logger } from "@tealfmbot/common/logger.ts"; 2 + import { CommandInteraction, SlashCommandBuilder } from "discord.js"; 3 4 export default { 5 data: new SlashCommandBuilder()
+3 -3
apps/bot/deploy-commands.ts
··· 1 - import { REST, Routes } from "discord.js"; 2 - import fs from "node:fs"; 3 - import path from "node:path"; 4 import { 5 DISCORD_APPLICATION_ID, 6 DISCORD_BOT_TOKEN, 7 DISCORD_GUILD_ID, 8 } from "@tealfmbot/common/constants.ts"; 9 10 const commands = []; 11 const commandPaths = fs.globSync("commands/**/*.ts");
··· 1 import { 2 DISCORD_APPLICATION_ID, 3 DISCORD_BOT_TOKEN, 4 DISCORD_GUILD_ID, 5 } from "@tealfmbot/common/constants.ts"; 6 + import { REST, Routes } from "discord.js"; 7 + import fs from "node:fs"; 8 + import path from "node:path"; 9 10 const commands = []; 11 const commandPaths = fs.globSync("commands/**/*.ts");
+2 -2
apps/bot/main.ts
··· 1 import { Client, Collection, Events, GatewayIntentBits, MessageFlags } from "discord.js"; 2 import fs from "node:fs"; 3 import path from "node:path"; 4 - import { DISCORD_BOT_TOKEN } from "@tealfmbot/common/constants.ts"; 5 - import { logger } from "@tealfmbot/common/logger.ts"; 6 7 const client = new Client({ intents: [GatewayIntentBits.Guilds] }); 8
··· 1 + import { DISCORD_BOT_TOKEN } from "@tealfmbot/common/constants.ts"; 2 + import { logger } from "@tealfmbot/common/logger.ts"; 3 import { Client, Collection, Events, GatewayIntentBits, MessageFlags } from "discord.js"; 4 import fs from "node:fs"; 5 import path from "node:path"; 6 7 const client = new Client({ intents: [GatewayIntentBits.Guilds] }); 8
+19 -2
apps/web/index.ts
··· 1 import { Hono } from "hono"; 2 - import { serve, type HttpBindings } from "@hono/node-server"; 3 4 - const app = new Hono<{ Bindings: HttpBindings }>(); 5 6 app.get("/", (c) => { 7 return c.text("yo!!"); 8 }); 9
··· 1 + import { serve, type HttpBindings } from "@hono/node-server"; 2 + import { logger } from "@tealfmbot/common/logger.js"; 3 import { Hono } from "hono"; 4 + import pinoHttpLogger from "pino-http" 5 + 6 + type Variables = { 7 + logger: typeof logger 8 + } 9 + 10 + const app = new Hono<{ Bindings: HttpBindings, Variables: Variables }>(); 11 + 12 + app.use(async (c, next) => { 13 + await new Promise<void>((resolve) => pinoHttpLogger({ 14 + autoLogging: false, 15 + })(c.env.incoming, c.env.outgoing, () => resolve())); 16 17 + c.set("logger", c.env.incoming.log) 18 + 19 + await next(); 20 + }) 21 22 app.get("/", (c) => { 23 + c.var.logger.info("test log") 24 return c.text("yo!!"); 25 }); 26
+2 -1
apps/web/package.json
··· 12 "@hono/node-server": "^1.19.7", 13 "@tealfmbot/common": "workspace:*", 14 "@tealfmbot/tsconfig": "workspace:*", 15 - "hono": "^4.11.3" 16 }, 17 "devDependencies": { 18 "@types/node": "^25.0.3",
··· 12 "@hono/node-server": "^1.19.7", 13 "@tealfmbot/common": "workspace:*", 14 "@tealfmbot/tsconfig": "workspace:*", 15 + "hono": "^4.11.3", 16 + "pino-http": "^11.0.0" 17 }, 18 "devDependencies": { 19 "@types/node": "^25.0.3",
+1 -1
packages/common/constants.ts
··· 1 - import { loadEnvFile } from "node:process"; 2 import path from "node:path"; 3 4 loadEnvFile(path.join(import.meta.dirname, "../../.env")); 5
··· 1 import path from "node:path"; 2 + import { loadEnvFile } from "node:process"; 3 4 loadEnvFile(path.join(import.meta.dirname, "../../.env")); 5
+3 -2
packages/database/db.ts
··· 1 import type { DB } from "kysely-codegen"; 2 import { Pool } from "pg"; 3 - import { Kysely, PostgresDialect } from "kysely"; 4 - import { DATABASE_URL } from "@tealfmbot/common/constants.ts"; 5 6 const dialect = new PostgresDialect({ 7 pool: new Pool({
··· 1 import type { DB } from "kysely-codegen"; 2 + 3 + import { DATABASE_URL } from "@tealfmbot/common/constants.ts"; 4 + import { Kysely, PostgresDialect } from "kysely"; 5 import { Pool } from "pg"; 6 7 const dialect = new PostgresDialect({ 8 pool: new Pool({
+5 -4
packages/database/migrator.ts
··· 1 import path from "node:path"; 2 import { Pool } from "pg"; 3 - import fs from "node:fs/promises"; 4 - import type { DB } from "kysely-codegen"; 5 - import { Kysely, Migrator, PostgresDialect, FileMigrationProvider } from "kysely"; 6 - import { DATABASE_URL } from "@tealfmbot/common/constants.ts"; 7 8 async function migrateToLatest() { 9 const db = new Kysely<DB>({
··· 1 + import type { DB } from "kysely-codegen"; 2 + 3 + import { DATABASE_URL } from "@tealfmbot/common/constants.ts"; 4 + import { Kysely, Migrator, PostgresDialect, FileMigrationProvider } from "kysely"; 5 + import fs from "node:fs/promises"; 6 import path from "node:path"; 7 import { Pool } from "pg"; 8 9 async function migrateToLatest() { 10 const db = new Kysely<DB>({
+19
pnpm-lock.yaml
··· 79 hono: 80 specifier: ^4.11.3 81 version: 4.11.3 82 devDependencies: 83 '@types/node': 84 specifier: ^25.0.3 ··· 636 function-bind@1.1.2: 637 resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 638 639 get-tsconfig@4.13.0: 640 resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 641 ··· 920 921 pino-abstract-transport@3.0.0: 922 resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} 923 924 pino-pretty@13.1.3: 925 resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} ··· 1567 1568 function-bind@1.1.2: {} 1569 1570 get-tsconfig@4.13.0: 1571 dependencies: 1572 resolve-pkg-maps: 1.0.0 ··· 1817 pino-abstract-transport@3.0.0: 1818 dependencies: 1819 split2: 4.2.0 1820 1821 pino-pretty@13.1.3: 1822 dependencies:
··· 79 hono: 80 specifier: ^4.11.3 81 version: 4.11.3 82 + pino-http: 83 + specifier: ^11.0.0 84 + version: 11.0.0 85 devDependencies: 86 '@types/node': 87 specifier: ^25.0.3 ··· 639 function-bind@1.1.2: 640 resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 641 642 + get-caller-file@2.0.5: 643 + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 644 + engines: {node: 6.* || 8.* || >= 10.*} 645 + 646 get-tsconfig@4.13.0: 647 resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} 648 ··· 927 928 pino-abstract-transport@3.0.0: 929 resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} 930 + 931 + pino-http@11.0.0: 932 + resolution: {integrity: sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==} 933 934 pino-pretty@13.1.3: 935 resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} ··· 1577 1578 function-bind@1.1.2: {} 1579 1580 + get-caller-file@2.0.5: {} 1581 + 1582 get-tsconfig@4.13.0: 1583 dependencies: 1584 resolve-pkg-maps: 1.0.0 ··· 1829 pino-abstract-transport@3.0.0: 1830 dependencies: 1831 split2: 4.2.0 1832 + 1833 + pino-http@11.0.0: 1834 + dependencies: 1835 + get-caller-file: 2.0.5 1836 + pino: 10.1.0 1837 + pino-std-serializers: 7.0.0 1838 + process-warning: 5.0.0 1839 1840 pino-pretty@13.1.3: 1841 dependencies: