A discord bot for teal.fm
discord tealfm music

holy fk docker works finally

besaid.zone dac1d2fe 5919e648

verified
+6
.dockerignore
··· 1 + node_modules 1 2 **/node_modules 2 3 **/*.md 4 + .git 5 + .gitignore 6 + **/dist 7 + .pnpm-store 8 + .DS_Store
+1
.env.example
··· 14 14 # oauth 15 15 PUBLIC_URL= 16 16 PRIVATE_KEYS= 17 + COOKIE_SECRET=
+19
Dockerfile
··· 1 + FROM node:24-alpine AS base 2 + ENV PNPM_HOME="/pnpm" 3 + ENV PATH="$PNPM_HOME:$PATH" 4 + ENV NODE_ENV=production 5 + RUN corepack enable 6 + 7 + FROM base AS build 8 + COPY . /app 9 + WORKDIR /app 10 + 11 + RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 12 + RUN pnpm run -r build 13 + RUN pnpm deploy --filter=./apps/web --prod /prod/web 14 + 15 + FROM base AS web 16 + COPY --from=build /prod/web /prod/web 17 + WORKDIR /prod/web 18 + EXPOSE 8002 19 + CMD ["pnpm", "start"]
+1 -1
apps/bot/commands/auth.ts
··· 1 - import { logger } from "@tealfmbot/common/logger.js"; 1 + import { logger } from "@tealfmbot/common/logger"; 2 2 import { 3 3 ChatInputCommandInteraction, 4 4 InteractionContextType,
+1 -1
apps/bot/commands/ping.ts
··· 1 - import { logger } from "@tealfmbot/common/logger.js"; 1 + import { logger } from "@tealfmbot/common/logger"; 2 2 import { CommandInteraction, SlashCommandBuilder } from "discord.js"; 3 3 4 4 export default {
+1 -1
apps/bot/commands/top.ts
··· 1 - import { logger } from "@tealfmbot/common/logger.js"; 1 + import { logger } from "@tealfmbot/common/logger"; 2 2 import { CommandInteraction, SlashCommandBuilder } from "discord.js"; 3 3 4 4 export default {
+1 -1
apps/bot/deploy-commands.ts
··· 2 2 DISCORD_APPLICATION_ID, 3 3 DISCORD_BOT_TOKEN, 4 4 DISCORD_GUILD_ID, 5 - } from "@tealfmbot/common/constants.js"; 5 + } from "@tealfmbot/common/constants"; 6 6 import { REST, Routes } from "discord.js"; 7 7 import fs from "node:fs"; 8 8 import path from "node:path";
+2 -2
apps/bot/main.ts
··· 1 - import { DISCORD_BOT_TOKEN } from "@tealfmbot/common/constants.js"; 2 - import { logger } from "@tealfmbot/common/logger.js"; 1 + import { DISCORD_BOT_TOKEN } from "@tealfmbot/common/constants"; 2 + import { logger } from "@tealfmbot/common/logger"; 3 3 import { Client, Collection, Events, GatewayIntentBits, MessageFlags } from "discord.js"; 4 4 import fs from "node:fs"; 5 5 import path from "node:path";
+1 -1
apps/tapper/index.ts
··· 1 1 import { SimpleIndexer, Tap } from "@atproto/tap"; 2 - import { TAP_ADMIN_PASSWORD } from "@tealfmbot/common/constants.js"; 2 + import { TAP_ADMIN_PASSWORD } from "@tealfmbot/common/constants"; 3 3 // import { db } from "./kysely/db.ts" 4 4 5 5 const tap = new Tap("https://tap.xero.systems", {
apps/web/Dockerfile

This is a binary file and will not be displayed.

+3 -3
apps/web/client.ts
··· 5 5 NodeOAuthClient, 6 6 type OAuthClientMetadataInput, 7 7 } from "@atproto/oauth-client-node"; 8 - import { PUBLIC_URL, PRIVATE_KEYS } from "@tealfmbot/common/constants.js"; 9 - import { db } from "@tealfmbot/database/db.ts"; 8 + import { PUBLIC_URL, PRIVATE_KEYS } from "@tealfmbot/common/constants"; 9 + import { db } from "@tealfmbot/database/db"; 10 10 import assert from "node:assert"; 11 11 12 - import { SessionStore, StateStore } from "./storage"; 12 + import { SessionStore, StateStore } from "./storage.js"; 13 13 14 14 const keyset = 15 15 PUBLIC_URL && PRIVATE_KEYS
+4 -4
apps/web/index.ts
··· 1 1 import { serve, type HttpBindings } from "@hono/node-server"; 2 - import { COOKIE_SECRET } from "@tealfmbot/common/constants.js"; 3 - import { logger } from "@tealfmbot/common/logger.js"; 2 + import { COOKIE_SECRET } from "@tealfmbot/common/constants"; 3 + import { logger } from "@tealfmbot/common/logger"; 4 4 import { Hono } from "hono"; 5 5 import { deleteCookie, getSignedCookie } from "hono/cookie"; 6 6 import { html } from "hono/html"; 7 7 import { validator } from "hono/validator"; 8 8 import pinoHttpLogger from "pino-http"; 9 9 10 - import { client } from "./client"; 11 - import { createSession, getSessionAgent, MAX_AGE, validateIdentifier } from "./utils"; 10 + import { client } from "./client.js"; 11 + import { createSession, getSessionAgent, MAX_AGE, validateIdentifier } from "./utils.js"; 12 12 13 13 type Variables = { 14 14 logger: typeof logger;
+1 -1
apps/web/package.json
··· 6 6 "scripts": { 7 7 "dev": "tsx --watch index.ts", 8 8 "build": "tsc", 9 - "start": "tsx index.ts", 9 + "start": "node dist/index.js", 10 10 "typecheck": "tsc --noEmit" 11 11 }, 12 12 "dependencies": {
+1 -1
apps/web/storage.ts
··· 4 4 NodeSavedState, 5 5 NodeSavedStateStore, 6 6 } from "@atproto/oauth-client-node"; 7 - import type { Database } from "@tealfmbot/database/types.ts"; 7 + import type { Database } from "@tealfmbot/database/types"; 8 8 9 9 export class StateStore implements NodeSavedStateStore { 10 10 constructor(private db: Database) {}
-3
apps/web/tsconfig.json
··· 3 3 "compilerOptions": { 4 4 "outDir": "./dist", 5 5 "target": "ESNext", 6 - "module": "ESNext", 7 - "moduleResolution": "bundler", 8 - "lib": ["ESNext"], 9 6 }, 10 7 "exclude": ["node_modules"] 11 8 }
+3 -3
apps/web/utils.ts
··· 3 3 import { Agent } from "@atproto/api"; 4 4 import { isAtprotoDid, isAtprotoDidWeb } from "@atproto/did"; 5 5 import { isValidHandle } from "@atproto/syntax"; 6 - import { COOKIE_SECRET } from "@tealfmbot/common/constants.js"; 7 - import { logger } from "@tealfmbot/common/logger.js"; 6 + import { COOKIE_SECRET } from "@tealfmbot/common/constants"; 7 + import { logger } from "@tealfmbot/common/logger"; 8 8 import { deleteCookie, generateSignedCookie, getSignedCookie } from "hono/cookie"; 9 9 10 - import { client } from "./client"; 10 + import { client } from "./client.js"; 11 11 12 12 export const MAX_AGE = process.env.NODE_ENV === "production" ? 60 : 0; 13 13
+1
package.json
··· 15 15 "bot": "pnpm --filter bot dev", 16 16 "tap": "pnpm --filter tapper dev", 17 17 "web": "pnpm --filter web dev", 18 + "docker:web": "docker build . --target web --tag web:latest", 18 19 "dev:all": "pnpm --filter './apps/**' dev", 19 20 "typecheck": "pnpm --filter './apps/**' typecheck", 20 21 "lint": "oxlint",
+3 -1
packages/common/constants.ts
··· 1 1 import path from "node:path"; 2 2 import { loadEnvFile } from "node:process"; 3 3 4 - loadEnvFile(path.join(import.meta.dirname, "../../.env")); 4 + if (process.env.NODE_ENV !== "production") { 5 + loadEnvFile(path.join(import.meta.dirname, "../../.env")); 6 + } 5 7 6 8 export const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN as string; 7 9 export const DISCORD_APPLICATION_ID = process.env.DISCORD_APPLICATION_ID as string;
+2
packages/common/index.ts
··· 1 + export * from "./constants.js"; 2 + export * from "./logger.js";
+11 -8
packages/common/logger.ts
··· 1 1 import pino from "pino"; 2 2 3 - export const logger = pino({ 4 - transport: { 5 - target: "pino-pretty", 6 - options: { 7 - colorize: true, 8 - }, 9 - }, 10 - }); 3 + export const logger = 4 + process.env.NODE_ENV === "production" 5 + ? pino() 6 + : pino({ 7 + transport: { 8 + target: "pino-pretty", 9 + options: { 10 + colorize: true, 11 + }, 12 + }, 13 + });
+10 -2
packages/common/package.json
··· 4 4 "private": true, 5 5 "type": "module", 6 6 "exports": { 7 - "./*": "./*" 8 - }, 7 + "./constants": { 8 + "types": "./dist/constants.d.ts", 9 + "import": "./dist/constants.js" 10 + }, 11 + "./logger": { 12 + "types": "./dist/logger.d.ts", 13 + "import": "./dist/logger.js" 14 + } 15 + }, 9 16 "scripts": { 17 + "build": "tsc", 10 18 "typecheck": "tsc --noEmit" 11 19 }, 12 20 "dependencies": {
+8 -1
packages/common/tsconfig.json
··· 1 1 { 2 - "extends": "@tealfmbot/tsconfig/tsconfig.node.json" 2 + "extends": "@tealfmbot/tsconfig/tsconfig.node.json", 3 + "compilerOptions": { 4 + "rootDir": ".", 5 + "declaration": true, 6 + "declarationMap": false, 7 + "emitDeclarationOnly": false, 8 + "outDir": "./dist" 9 + } 3 10 }
+51
packages/database/database.d.ts
··· 1 + /** 2 + * This file was generated by kysely-codegen. 3 + * Please do not edit it manually. 4 + */ 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 + 15 + export interface Artists { 16 + artist_name: string; 17 + play_id: number; 18 + } 19 + 20 + export interface AuthSession { 21 + key: string; 22 + session: string; 23 + } 24 + 25 + export interface AuthState { 26 + key: string; 27 + state: string; 28 + } 29 + 30 + export interface Plays { 31 + id: Generated<number>; 32 + indexed_at: Generated<Timestamp>; 33 + played_time: Timestamp; 34 + release_name: string; 35 + track_name: string; 36 + user_id: number; 37 + } 38 + 39 + export interface Users { 40 + created_at: Generated<Timestamp>; 41 + did: string; 42 + id: Generated<number>; 43 + } 44 + 45 + export interface DB { 46 + artists: Artists; 47 + auth_session: AuthSession; 48 + auth_state: AuthState; 49 + plays: Plays; 50 + users: Users; 51 + }
+3 -3
packages/database/db.ts
··· 1 - import type { DB } from "kysely-codegen"; 2 - 3 - import { DATABASE_URL } from "@tealfmbot/common/constants.js"; 1 + import { DATABASE_URL } from "@tealfmbot/common/constants"; 4 2 import { Kysely, PostgresDialect } from "kysely"; 5 3 import { Pool } from "pg"; 4 + 5 + import type { DB } from "./database.js"; 6 6 7 7 const dialect = new PostgresDialect({ 8 8 pool: new Pool({
+3 -3
packages/database/migrate.ts
··· 1 - import type { DB } from "kysely-codegen"; 2 - 3 - import { DATABASE_URL } from "@tealfmbot/common/constants.ts"; 1 + import { DATABASE_URL } from "@tealfmbot/common/constants.js"; 4 2 import { Kysely, Migrator, PostgresDialect, FileMigrationProvider } from "kysely"; 5 3 import { run } from "kysely-migration-cli"; 6 4 import fs from "node:fs/promises"; 7 5 import path from "node:path"; 8 6 import { Pool } from "pg"; 7 + 8 + import type { DB } from "./database.js"; 9 9 10 10 async function migrateToLatest() { 11 11 const migrationFolder = new URL("../migrations", import.meta.url).pathname;
+12 -4
packages/database/package.json
··· 4 4 "private": true, 5 5 "type": "module", 6 6 "exports": { 7 - "./db.ts": "./db.ts", 8 - "./types.ts": "./types.ts" 9 - }, 7 + "./db": { 8 + "types": "./dist/db.d.ts", 9 + "import": "./dist/db.js" 10 + }, 11 + "./types": { 12 + "types": "./dist/types.d.ts", 13 + "import": "./dist/types.js" 14 + } 15 + }, 16 + "types": "./database.d.ts", 10 17 "scripts": { 18 + "build": "tsc", 11 19 "migrate": "kysely migrate latest", 12 - "codegen": "kysely-codegen --dialect postgres --env-file='../../.env'", 20 + "codegen": "kysely-codegen --dialect postgres --env-file='../../.env' --out-file ./database.d.ts", 13 21 "seed": "tsx seed.ts", 14 22 "typecheck": "tsc --noEmit" 15 23 },
+9 -1
packages/database/tsconfig.json
··· 1 1 { 2 2 "extends": "@tealfmbot/tsconfig/tsconfig.node.json", 3 - "exclude": ["node_modules"] 3 + "exclude": ["node_modules"], 4 + "compilerOptions": { 5 + "rootDir": ".", 6 + "declaration": true, 7 + "declarationMap": false, 8 + "emitDeclarationOnly": false, 9 + "outDir": "./dist" 10 + }, 11 + "include": ["db.ts", "database.d.ts", "types.ts"] 4 12 }
+3 -7
packages/database/types.ts
··· 1 - // @ts-nocheck 2 - import type { Artists, Plays } from "kysely-codegen"; 1 + import type { Kysely } from "kysely"; 3 2 4 - import { db } from "./db.ts"; 3 + import type { DB } from "./database.js"; 5 4 6 - export type Database = typeof db; 7 - export interface TealFMPlay extends Plays { 8 - artists: Artists; 9 - } 5 + export type Database = Kysely<DB>;
+2
packages/tsconfig/tsconfig.node.json
··· 1 1 { 2 2 "extends": "./tsconfig.base.json", 3 3 "compilerOptions": { 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 4 6 "lib": ["esnext"], 5 7 "types": ["node"] 6 8 }
+1
pnpm-lock.yaml
··· 3 3 settings: 4 4 autoInstallPeers: true 5 5 excludeLinksFromLockfile: false 6 + injectWorkspacePackages: true 6 7 7 8 importers: 8 9
+7
pnpm-workspace.yaml
··· 4 4 packages: 5 5 - "apps/*" 6 6 - "packages/*" 7 + 8 + blockExoticSubdeps: true 9 + minimumReleaseAge: 10080 10 + trustPolicy: no-downgrade 11 + syncInjectedDepsAfterScripts: 12 + - build 13 + injectWorkspacePackages: true