A discord bot for teal.fm
discord tealfm music

fixing session related stuff

besaid.zone cea49dd9 e4fcf3e1

verified
+3 -2
Dockerfile
··· 10 10 ARG BUILD_DATE 11 11 ARG SHA 12 12 ARG VERSION 13 + ARG ATPROTO_HANDLE 13 14 14 15 FROM base AS build 15 16 COPY --chown=1000:1000 apps/bot /app ··· 36 37 EXPOSE 8002 37 38 CMD ["node", "dist/index.js"] 38 39 LABEL org.opencontainers.image.authors="Dane Miller 'me@dane.computer'" \ 39 - org.opencontainers.image.source="https://tangled.org/dane.is.extraordinarily.cool/tealfmbot" \ 40 + org.opencontainers.image.source="https://tangled.org/${ATPROTO_HANDLE}/tealfmbot" \ 40 41 org.opencontainers.image.title="discostuweb" \ 41 42 org.opencontainers.image.description="The web service for authentication for the disco stu discord bot" \ 42 43 org.opencontainers.image.version=$VERSION \ ··· 50 51 WORKDIR /prod/bot 51 52 CMD ["node", "dist/main.js"] 52 53 LABEL org.opencontainers.image.authors="Dane Miller 'me@dane.computer'" \ 53 - org.opencontainers.image.source="https://tangled.org/dane.is.extraordinarily.cool/tealfmbot" \ 54 + org.opencontainers.image.source="https://tangled.org/${ATPROTO_HANDLE}/tealfmbot" \ 54 55 org.opencontainers.image.title="discostubot" \ 55 56 org.opencontainers.image.description="A discord bot that displays your music listens based on your teal.fm records" \ 56 57 org.opencontainers.image.version=$VERSION \
+7 -2
apps/bot/commands/auth.ts
··· 2 2 import { logger } from "@tealfmbot/common/logger"; 3 3 import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; 4 4 5 + const LOGIN_URL = 6 + process.env.NODE_ENV === "development" ? `127.0.0.1:${env.WEB_SERVICE_PORT}` : env.PUBLIC_URL; 7 + 5 8 export default { 6 9 data: new SlashCommandBuilder() 7 10 .setName("login") ··· 9 12 "Authenticate with your Atmosphere account and request your listens to be tracked", 10 13 ), 11 14 async execute(interaction: ChatInputCommandInteraction) { 12 - await interaction.reply(`Log in here and request to be tracked: ${env.WEB_SERVICE_URL}:${env.WEB_SERVICE_PORT}/login`); 13 - logger.info(`starting authentication process for ${interaction.user.globalName} at ${new Date().toJSON()}`); 15 + await interaction.reply(`Log in here and request to be tracked: ${LOGIN_URL}/login`); 16 + logger.info( 17 + `starting authentication process for ${interaction.user.username} in guild ${interaction.guildId} at ${new Date().toJSON()}`, 18 + ); 14 19 }, 15 20 };
+1 -3
apps/bot/deploy-commands.ts
··· 1 - import { 2 - env 3 - } from "@tealfmbot/common/constants"; 1 + import { env } from "@tealfmbot/common/constants"; 4 2 import { REST, Routes } from "discord.js"; 5 3 import fs from "node:fs"; 6 4 import path from "node:path";
+1 -1
apps/bot/package.json
··· 4 4 "private": true, 5 5 "type": "module", 6 6 "scripts": { 7 - "dev": "tsx --watch main.ts", 7 + "dev": "NODE_ENV=development tsx --watch main.ts", 8 8 "deploy-commands": "tsx deploy-commands.ts", 9 9 "build": "tsc", 10 10 "watch": "tsc --watch",
+10 -14
apps/web/client.ts
··· 8 8 import { env } from "@tealfmbot/common/constants"; 9 9 import { db } from "@tealfmbot/database/db"; 10 10 import assert from "node:assert"; 11 + 11 12 import { SessionStore, StateStore } from "./storage.js"; 12 13 13 14 const loadJwk = async () => { 14 - const raw = env.PRIVATE_KEYS; 15 - if (!raw) return undefined; 16 - const json = JSON.parse(raw); 17 - if (!json) return undefined; 18 - const keys = await Promise.all( 19 - json.map((jwk: string | Record<string, unknown>) => JoseKey.fromJWK(jwk)), 20 - ); 21 - return new Keyset(keys); 15 + const raw = env.PRIVATE_KEYS; 16 + if (!raw) return undefined; 17 + const json = JSON.parse(raw); 18 + if (!json) return undefined; 19 + const keys = await Promise.all( 20 + json.map((jwk: string | Record<string, unknown>) => JoseKey.fromJWK(jwk)), 21 + ); 22 + return new Keyset(keys); 22 23 }; 23 24 24 - const keyset = 25 - env.PUBLIC_URL && env.PRIVATE_KEYS 26 - ? await loadJwk() 27 - : undefined; 28 - 29 - 25 + const keyset = env.PUBLIC_URL && env.PRIVATE_KEYS ? await loadJwk() : undefined; 30 26 31 27 assert(!env.PUBLIC_URL || keyset?.size, "PRIVATE_KEYS environment variable must be set"); 32 28
+28 -15
apps/web/index.ts
··· 8 8 import pinoHttpLogger from "pino-http"; 9 9 10 10 import { client } from "./client.js"; 11 - import { createSession, getSessionAgent, MAX_AGE, validateIdentifier } from "./utils.js"; 11 + import { 12 + createSession, 13 + getSessionAgent, 14 + MAX_AGE, 15 + validateIdentifier, 16 + getSession, 17 + } from "./utils.js"; 12 18 13 19 type Variables = { 14 20 logger: typeof logger; ··· 29 35 }); 30 36 31 37 app.get("/dashboard", async (c) => { 32 - const agent = await getSessionAgent(c); 33 - if (agent?.assertAuthenticated()) { 34 - return c.redirect("/login"); 38 + const session = await getSession(c, env); 39 + if (session) { 40 + const agent = await getSessionAgent(c); 41 + return c.html(html` 42 + <h1>Dashboard</h1> 43 + <p>DID: ${agent?.assertDid}</p> 44 + <form method="post" action="/logout"> 45 + <button type="submit">Log out</button> 46 + </form> 47 + `); 35 48 } 36 - return c.html(html` 37 - <h1>Dashboard</h1> 38 - <p>DID: ${agent?.assertDid}</p> 39 - <form method="post" action="/logout"> 40 - <button type="submit">Log out</button> 41 - </form> 42 - `); 49 + return c.redirect("/login"); 43 50 }); 44 51 45 52 app.get("/health", (c) => { ··· 61 68 const params = new URLSearchParams(c.req.url.split("?")[1]); 62 69 63 70 try { 64 - const session = await getSignedCookie(c, env.COOKIE_SECRET, "__teal_fm_bot_session"); 71 + const session = await getSession(c, env); 65 72 if (session) { 66 73 try { 67 74 const oauthSession = await client.restore(session); ··· 80 87 return c.redirect("/dashboard"); 81 88 }); 82 89 83 - app.get("/login", (c) => { 90 + app.get("/login", async (c) => { 91 + const session = await getSession(c, env); 92 + if (session) { 93 + return c.redirect("/dashboard") 94 + } 95 + 84 96 return c.html( 85 97 html` 86 98 <form action="/login" method="post"> ··· 90 102 </form> 91 103 `, 92 104 ); 105 + 93 106 }); 94 107 95 108 app.post( ··· 114 127 }); 115 128 return c.redirect(url.href.toString()); 116 129 } catch (error) { 117 - logger.error({error}, "oauth authorize failed"); 130 + logger.error({ error }, "oauth authorize failed"); 118 131 return c.json({ message: "oauth authorize failed" }, 500); 119 132 } 120 133 }, ··· 122 135 123 136 app.post("/logout", async (c) => { 124 137 c.header("Cache-Control", "no-store"); 125 - const session = await getSignedCookie(c, env.COOKIE_SECRET, "__teal_fm_bot_session"); 138 + const session = await getSession(c, env); 126 139 if (session) { 127 140 try { 128 141 const oauthSession = await client.restore(session);
+5
apps/web/utils.ts
··· 39 39 } 40 40 } 41 41 42 + export async function getSession(c: Context, e: typeof env) { 43 + const session = await getSignedCookie(c, e.COOKIE_SECRET, "__teal_fm_bot_session"); 44 + return session; 45 + } 46 + 42 47 export function isValidUrl(url: string) { 43 48 if (URL.canParse(url)) { 44 49 const pdsUrl = new URL(url);
+2
build-and-publish-images.sh
··· 2 2 BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) 3 3 VERSION=$(git describe --tags --abbrev=0) 4 4 REGISTRY=atcr.io/besaid.zone 5 + ATPROTO_HANDLE=besaid.zone 5 6 6 7 services=( 7 8 web ··· 17 18 --target $svc \ 18 19 --build-arg VERSION=${VERSION#v} \ 19 20 --build-arg SHA=$SHA \ 21 + --build-arg ATPROTO_HANDLE=$ATPROTO_HANDLE \ 20 22 --build-arg BUILD_DATE=$BUILD_DATE \ 21 23 --pull \ 22 24 --no-cache \
+1 -1
docker-compose.dev.yml
··· 1 1 services: 2 - db: 2 + db: 3 3 image: postgres:18.1 4 4 healthcheck: 5 5 test: ["CMD-SHELL", "pg_isready -U postgres -d tealfmbotdb"]
+2 -3
packages/common/constants.ts
··· 13 13 TAP_ADMIN_PASSWORD: str(), 14 14 DATABASE_URL: str({ devDefault: "postgres://postgres:password@localhost:5432/tealfmbotdb" }), 15 15 PUBLIC_URL: str(), 16 - COOKIE_SECRET: str({ devDefault: '00000000000000000000000000000000' }), 16 + COOKIE_SECRET: str({ devDefault: "00000000000000000000000000000000" }), 17 17 PRIVATE_KEYS: str(), 18 - WEB_SERVICE_URL: str({ devDefault: "http://localhost" }), 19 - WEB_SERVICE_PORT: num({devDefault: 8002}) 18 + WEB_SERVICE_PORT: num({ devDefault: 8002 }), 20 19 });