A discord bot for teal.fm
discord tealfm music
at main 4.3 kB view raw
1import { serve, type HttpBindings } from "@hono/node-server"; 2import { env } from "@tealfmbot/common/constants"; 3import { logger } from "@tealfmbot/common/logger"; 4import { Hono } from "hono"; 5import { deleteCookie, getSignedCookie } from "hono/cookie"; 6import { html } from "hono/html"; 7import { validator } from "hono/validator"; 8import pinoHttpLogger from "pino-http"; 9 10import { client } from "./client.js"; 11import { 12 createSession, 13 getSessionAgent, 14 MAX_AGE, 15 validateIdentifier, 16 getSession, 17} from "./utils.js"; 18 19type Variables = { 20 logger: typeof logger; 21}; 22 23const app = new Hono<{ Bindings: HttpBindings; Variables: Variables }>(); 24 25app.use(async (c, next) => { 26 await new Promise<void>((resolve) => 27 pinoHttpLogger({ 28 autoLogging: false, 29 })(c.env.incoming, c.env.outgoing, () => resolve()), 30 ); 31 32 c.set("logger", c.env.incoming.log); 33 34 await next(); 35}); 36 37app.get("/dashboard", async (c) => { 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 `); 48 } 49 return c.redirect("/login"); 50}); 51 52app.get("/health", (c) => { 53 return c.json({ message: "OK" }, 200); 54}); 55 56app.get("/oauth-client-metadata.json", (c) => { 57 c.header("Cache-Control", `max-age=${MAX_AGE}, public`); 58 return c.json(client.clientMetadata); 59}); 60 61app.get("/.well-known/jwks.json", (c) => { 62 c.header("Cache-Control", `max-age=${MAX_AGE}, public`); 63 return c.json(client.jwks); 64}); 65 66app.get("/oauth/callback", async (c) => { 67 c.header("Cache-Control", "no-store"); 68 const params = new URLSearchParams(c.req.url.split("?")[1]); 69 70 try { 71 const session = await getSession(c, env); 72 if (session) { 73 try { 74 const oauthSession = await client.restore(session); 75 if (oauthSession) oauthSession.signOut(); 76 } catch (error) { 77 logger.warn({ error }, "oauth restore failed"); 78 } 79 } 80 const oauth = await client.callback(params); 81 const cookie = await createSession(oauth.session.did); 82 c.header("Set-Cookie", cookie); 83 } catch (error) { 84 logger.error({ error }, "oauth callback failed"); 85 } 86 87 return c.redirect("/dashboard"); 88}); 89 90app.get("/login", async (c) => { 91 const session = await getSession(c, env); 92 if (session) { 93 return c.redirect("/dashboard"); 94 } 95 96 return c.html( 97 html` 98 <form action="/login" method="post"> 99 <label for="identifier">Identifier</label> 100 <input id="identifier" name="identifier" type="text" placeholder="handle.bsky.social" required /> 101 <button type="submit">Log in</button> 102 </form> 103 `, 104 ); 105}); 106 107app.post( 108 "/login", 109 validator("form", (value, c) => { 110 const identifier = value["identifier"]; 111 if (typeof identifier !== "string" || !validateIdentifier(identifier)) { 112 return c.json({ message: "Invalid handle, did or PDS URL" }, 400); 113 } 114 115 return { 116 identifier, 117 }; 118 }), 119 async (c) => { 120 c.header("Cache-Control", "no-store"); 121 const { identifier } = c.req.valid("form"); 122 const ac = new AbortController(); 123 try { 124 const url = await client.authorize(identifier, { 125 signal: ac.signal, 126 }); 127 return c.redirect(url.href.toString()); 128 } catch (error) { 129 logger.error({ error }, "oauth authorize failed"); 130 return c.json({ message: "oauth authorize failed" }, 500); 131 } 132 }, 133); 134 135app.post("/logout", async (c) => { 136 c.header("Cache-Control", "no-store"); 137 const session = await getSession(c, env); 138 if (session) { 139 try { 140 const oauthSession = await client.restore(session); 141 if (oauthSession) await oauthSession.signOut(); 142 } catch (error) { 143 logger.warn({ error }, "Failed to revoke credentials"); 144 } 145 } 146 147 deleteCookie(c, "__teal_fm_bot_session"); 148 149 return c.redirect("/login"); 150}); 151 152const server = serve({ 153 fetch: app.fetch, 154 port: 8002, 155}); 156 157process.on("SIGINT", () => { 158 server.close(); 159 process.exit(0); 160}); 161process.on("SIGTERM", () => { 162 server.close((err) => { 163 if (err) { 164 console.error(err); 165 process.exit(1); 166 } 167 process.exit(0); 168 }); 169});