import express from 'express'; import * as log from './lib/log'; import { CustomItem, Session, db } from './lib/db'; import { defaultItems } from './lib/rpg/items'; import { Client, RESTPostOAuth2AccessTokenResult, RESTPostOAuth2AccessTokenURLEncodedData, RESTPostOAuth2RefreshTokenURLEncodedData, Routes } from 'discord.js'; import got from 'got'; import uid from 'uid-safe'; import { Cookie, parse } from 'tough-cookie'; import { IncomingHttpHeaders } from 'http'; const DISCORD_ENDPOINT = 'https://discord.com/api/v10'; const UID_BYTE_LENGTH = 18; const COOKIE_KEY = 'PHPSESSID'; const COOKIE_EXPIRES = 1_000 * 60 * 60 * 24 * 365; async function getSessionString(cookieStr: string) { const cookies = cookieStr.split('; ').map(s => parse(s)!).filter(c => c !== null); const sessionCookie = cookies.find(c => c.key === COOKIE_KEY); if (!sessionCookie) { return await uid(UID_BYTE_LENGTH); } else { return sessionCookie.value; } } async function getSession(bot: Client, headers: IncomingHttpHeaders) { const cookie = headers['cookie']; if (!cookie) return; const sessionStr = await getSessionString(cookie); const session = await db('sessions') .where('id', sessionStr) .first(); if (!session) return; if (Date.now() < session.expiresAt) return session; let resp; try { resp = await got.post(DISCORD_ENDPOINT + Routes.oauth2TokenExchange(), { form: { client_id: bot.config.clientId, client_secret: bot.config.clientSecret, grant_type: 'refresh_token', refresh_token: session.refreshToken, } satisfies RESTPostOAuth2RefreshTokenURLEncodedData }).json() as RESTPostOAuth2AccessTokenResult; } catch(err) { log.error(err); return; } const sessionData = { tokenType: resp.token_type, accessToken: resp.access_token, refreshToken: resp.refresh_token, expiresAt: Date.now() + resp.expires_in * 1000, }; return (await db('sessions') .where('id', sessionStr) .update(sessionData) .returning('*'))[0]; } export async function startServer(bot: Client, port: number) { const app = express(); app.get('/api/items', async (req, res) => { const guildID = req.query.guild; let customItems : Partial[]; if (guildID) { customItems = await db('customItems') .select('emoji', 'name', 'id', 'description') .where('guild', guildID) .limit(25); } else { customItems = []; } res.json([...defaultItems, ...customItems]); }); app.get('/api/status', async (_, res) => { res.json({ guilds: bot.guilds.cache.size, uptime: bot.uptime }); }); app.get('/', async (req, res) => { const code = req.query.code as string; if (code) { try { const resp = await got.post(DISCORD_ENDPOINT + Routes.oauth2TokenExchange(), { form: { client_id: bot.config.clientId, client_secret: bot.config.clientSecret, code, grant_type: 'authorization_code', redirect_uri: bot.config.siteURL, } satisfies RESTPostOAuth2AccessTokenURLEncodedData // if you're looking to change this then you are blissfully unaware of the past // and have learnt 0 lessons }).json() as RESTPostOAuth2AccessTokenResult; const sessionId = await getSessionString(decodeURIComponent(req.headers.cookie || '')); const session = await db('sessions') .where('id', sessionId) .first(); const sessionData = { tokenType: resp.token_type, accessToken: resp.access_token, refreshToken: resp.refresh_token, expiresAt: Date.now() + resp.expires_in * 1000, }; if (session) { await db('sessions') .where('id', sessionId) .update(sessionData); } else { await db('sessions') .insert({id: sessionId, ...sessionData} satisfies Session); } const cookie = new Cookie({ key: COOKIE_KEY, value: sessionId, expires: new Date(Date.now() + COOKIE_EXPIRES), sameSite: 'strict' }); res.setHeader('Set-Cookie', cookie.toString()); return res.redirect('/profile'); } catch (err) { log.error(err); return res.status(500); } } res.sendFile('index.html', { root: 'static/' }); }); app.get('/profile', async (req, res) => { const session = await getSession(bot, req.headers); if (!session) return res.redirect(`https://discord.com/api/oauth2/authorize?client_id=${bot.config.clientId}&redirect_uri=${encodeURIComponent(bot.config.siteURL)}&response_type=code&scope=identify%20guilds`); const user = await got('https://discord.com/api/users/@me', { headers: { authorization: `${session.tokenType} ${session.accessToken}` } }).json(); //res.sendFile('profile/index.html', { root: 'static/' }); res.json(user); }); app.use(express.static('static/')); app.listen(port, () => log.info(`web interface listening on ${port}`)); }