From cddcfee26ee10717dda918a9a1864a076a93a80c Mon Sep 17 00:00:00 2001 From: "Jill \"oatmealine\" Monoids" Date: Mon, 20 Nov 2023 04:56:18 +0300 Subject: [PATCH] move stuff into seperate files --- src/index.ts | 2 +- src/web.ts | 232 ---------------------------------------------- src/web/oauth2.ts | 116 +++++++++++++++++++++++ src/web/user.ts | 32 +++++++ src/web/web.ts | 95 +++++++++++++++++++ 5 files changed, 244 insertions(+), 233 deletions(-) delete mode 100644 src/web.ts create mode 100644 src/web/oauth2.ts create mode 100644 src/web/user.ts create mode 100644 src/web/web.ts diff --git a/src/index.ts b/src/index.ts index 03590b3..d16e1f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import * as log from './lib/log'; import chalk from 'chalk'; import prettyBytes from 'pretty-bytes'; import { Command } from './types/index'; -import { startServer } from './web'; +import { startServer } from './web/web'; const bot = new Client({ intents: [ diff --git a/src/web.ts b/src/web.ts deleted file mode 100644 index 9acca65..0000000 --- a/src/web.ts +++ /dev/null @@ -1,232 +0,0 @@ -import express from 'express'; -import { engine } from 'express-handlebars'; -import * as log from './lib/log'; -import { CustomItem, Session, db } from './lib/db'; -import { defaultItems } from './lib/rpg/items'; -import { APIPartialGuild, APIUser, CDN, 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 UID_STRING_LENGTH = 24; // why? -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 || sessionCookie.value.length !== UID_STRING_LENGTH) { - return await uid(UID_BYTE_LENGTH); - } else { - return sessionCookie.value; - } -} - -async function setSession(sessionId: string, sessionData: Omit) { - const session = await db('sessions') - .where('id', sessionId) - .first(); - - if (session) { - await db('sessions') - .where('id', sessionId) - .update(sessionData); - } else { - await db('sessions') - .insert({id: sessionId, ...sessionData}) - .returning('*'); - } -} - -async function getToken(bot: Client, code: string) { - try { - return 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 - } catch(err) { - log.error(err); - return; - } -} - -async function refreshToken(bot: Client, sessionId: string, refreshToken: string) { - 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: 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', sessionId) - .update(sessionData) - .returning('*'))[0]; -} - -function updateCookie(res: express.Response, sessionId: string) { - const cookie = new Cookie({ - key: COOKIE_KEY, - value: sessionId, - expires: new Date(Date.now() + COOKIE_EXPIRES), - sameSite: 'strict' - }); - res.setHeader('Set-Cookie', cookie.toString()); -} - -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; - - const newSession = refreshToken(bot, session.id, session.refreshToken); -} - -export async function getUser(session: Session | undefined) { - if (!session) return null; - try { - return await got('https://discord.com/api/users/@me', { - headers: { - authorization: `${session.tokenType} ${session.accessToken}` - } - }).json() as APIUser; - } catch(err) { - log.error(err); - return null; - } -} -export async function getGuilds(session: Session | undefined) { - if (!session) return null; - try { - return await got('https://discord.com/api/users/@me/guilds', { - headers: { - authorization: `${session.tokenType} ${session.accessToken}` - } - }).json() as APIPartialGuild[]; - } catch(err) { - log.error(err); - return null; - } -} - -export async function startServer(bot: Client, port: number) { - const app = express(); - const cdn = new CDN(); - - app.engine('handlebars', engine()); - app.set('view engine', 'handlebars'); - app.set('views', './views'); - - 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 getToken(bot, code); - if (!resp) return res.status(400).send('Invalid code provided'); - - const sessionId = await getSessionString(decodeURIComponent(req.headers.cookie || '')); - - setSession(sessionId, { - tokenType: resp.token_type, - accessToken: resp.access_token, - refreshToken: resp.refresh_token, - expiresAt: Date.now() + resp.expires_in * 1000, - }); - - updateCookie(res, sessionId); - - return res.redirect('/profile'); - } catch (err) { - log.error(err); - return res.status(500); - } - } - - const session = await getSession(bot, req.headers); - const user = await getUser(session); - - res.render('home', { - signedIn: session !== undefined, - username: user?.global_name, - avatar: user?.avatar ? cdn.avatar(user.id, user.avatar, { size: 128 }) : null, - layout: false, - }); - }); - - 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 getUser(session); - const guilds = await getGuilds(session); - - //res.sendFile('profile/index.html', { root: 'static/' }); - res.json({ - user, - guilds - }); - }); - - app.use(express.static('static/')); - - app.listen(port, () => log.info(`web interface listening on ${port}`)); -} \ No newline at end of file diff --git a/src/web/oauth2.ts b/src/web/oauth2.ts new file mode 100644 index 0000000..a5782de --- /dev/null +++ b/src/web/oauth2.ts @@ -0,0 +1,116 @@ +import type { Response } from 'express'; +import type { IncomingHttpHeaders } from 'http'; +import * as log from '../lib/log'; +import { Cookie, parse as parseCookie } from 'tough-cookie'; +import uid from 'uid-safe'; +import { Client, RESTPostOAuth2AccessTokenResult, RESTPostOAuth2AccessTokenURLEncodedData, RESTPostOAuth2RefreshTokenURLEncodedData, Routes } from 'discord.js'; +import got from 'got'; +import { Session, db } from '../lib/db'; + +export const DISCORD_ENDPOINT = 'https://discord.com/api/v10'; +const UID_BYTE_LENGTH = 18; +const UID_STRING_LENGTH = 24; // why? +const COOKIE_KEY = 'PHPSESSID'; +const COOKIE_EXPIRES = 1_000 * 60 * 60 * 24 * 365; + +export async function getSessionString(cookieStr: string) { + const cookies = cookieStr.split('; ').map(s => parseCookie(s)!).filter(c => c !== null); + const sessionCookie = cookies.find(c => c.key === COOKIE_KEY); + + if (!sessionCookie || sessionCookie.value.length !== UID_STRING_LENGTH) { + return await uid(UID_BYTE_LENGTH); + } else { + return sessionCookie.value; + } +} + +export function updateCookie(res: Response, sessionId: string) { + const cookie = new Cookie({ + key: COOKIE_KEY, + value: sessionId, + expires: new Date(Date.now() + COOKIE_EXPIRES), + sameSite: 'strict' + }); + res.setHeader('Set-Cookie', cookie.toString()); +} + +export async function getToken(bot: Client, code: string) { + try { + return 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 + } catch(err) { + log.error(err); + return; + } +} + +async function refreshToken(bot: Client, sessionId: string, refreshToken: string) { + 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: 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', sessionId) + .update(sessionData) + .returning('*'))[0]; +} + +export 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; + + const newSession = refreshToken(bot, session.id, session.refreshToken); +} + +export async function setSession(sessionId: string, sessionData: Omit) { + const session = await db('sessions') + .where('id', sessionId) + .first(); + + if (session) { + await db('sessions') + .where('id', sessionId) + .update(sessionData); + } else { + await db('sessions') + .insert({id: sessionId, ...sessionData}) + .returning('*'); + } +} \ No newline at end of file diff --git a/src/web/user.ts b/src/web/user.ts new file mode 100644 index 0000000..ee6989b --- /dev/null +++ b/src/web/user.ts @@ -0,0 +1,32 @@ +import { Session, db } from '../lib/db'; +import * as log from '../lib/log'; +import got from 'got'; +import { APIPartialGuild, APIUser, Routes } from 'discord.js'; +import { DISCORD_ENDPOINT } from './oauth2'; + +export async function getUser(session: Session | undefined) { + if (!session) return null; + try { + return await got(DISCORD_ENDPOINT + Routes.user(), { + headers: { + authorization: `${session.tokenType} ${session.accessToken}` + } + }).json() as APIUser; + } catch(err) { + log.error(err); + return null; + } +} +export async function getGuilds(session: Session | undefined) { + if (!session) return null; + try { + return await got(DISCORD_ENDPOINT + Routes.userGuilds(), { + headers: { + authorization: `${session.tokenType} ${session.accessToken}` + } + }).json() as APIPartialGuild[]; + } catch(err) { + log.error(err); + return null; + } +} \ No newline at end of file diff --git a/src/web/web.ts b/src/web/web.ts new file mode 100644 index 0000000..2583f11 --- /dev/null +++ b/src/web/web.ts @@ -0,0 +1,95 @@ +import express from 'express'; +import { engine } from 'express-handlebars'; +import * as log from '../lib/log'; +import { CustomItem, Session, db } from '../lib/db'; +import { defaultItems } from '../lib/rpg/items'; +import { Client, CDN } from 'discord.js'; +import { getToken, getSessionString, getSession, setSession, updateCookie } from './oauth2'; +import { getUser, getGuilds } from './user'; + +export async function startServer(bot: Client, port: number) { + const app = express(); + const cdn = new CDN(); + + app.engine('handlebars', engine()); + app.set('view engine', 'handlebars'); + app.set('views', './views'); + + 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 getToken(bot, code); + if (!resp) return res.status(400).send('Invalid code provided'); + + const sessionId = await getSessionString(decodeURIComponent(req.headers.cookie || '')); + + setSession(sessionId, { + tokenType: resp.token_type, + accessToken: resp.access_token, + refreshToken: resp.refresh_token, + expiresAt: Date.now() + resp.expires_in * 1000, + }); + + updateCookie(res, sessionId); + + return res.redirect('/profile'); + } catch (err) { + log.error(err); + return res.status(500); + } + } + + const session = await getSession(bot, req.headers); + const user = await getUser(session); + + res.render('home', { + signedIn: session !== undefined, + username: user?.global_name, + avatar: user?.avatar ? cdn.avatar(user.id, user.avatar, { size: 128 }) : null, + layout: false, + }); + }); + + 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 getUser(session); + const guilds = await getGuilds(session); + + //res.sendFile('profile/index.html', { root: 'static/' }); + res.json({ + user, + guilds + }); + }); + + app.use(express.static('static/')); + + app.listen(port, () => log.info(`web interface listening on ${port}`)); +} \ No newline at end of file