jillo-bot/src/web.ts

169 lines
5.1 KiB
TypeScript

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<Session>('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<Session>('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<CustomItem>[];
if (guildID) {
customItems = await db<CustomItem>('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<Session>('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<Session>('sessions')
.where('id', sessionId)
.update(sessionData);
} else {
await db<Session>('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}`));
}