move stuff into seperate files
This commit is contained in:
parent
6d31321c14
commit
cddcfee26e
|
@ -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: [
|
||||
|
|
232
src/web.ts
232
src/web.ts
|
@ -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<Session, 'id'>) {
|
||||
const session = await db<Session>('sessions')
|
||||
.where('id', sessionId)
|
||||
.first();
|
||||
|
||||
if (session) {
|
||||
await db<Session>('sessions')
|
||||
.where('id', sessionId)
|
||||
.update(sessionData);
|
||||
} else {
|
||||
await db<Session>('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<Session>('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<Session>('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<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 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}`));
|
||||
}
|
|
@ -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<Session>('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<Session>('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<Session, 'id'>) {
|
||||
const session = await db<Session>('sessions')
|
||||
.where('id', sessionId)
|
||||
.first();
|
||||
|
||||
if (session) {
|
||||
await db<Session>('sessions')
|
||||
.where('id', sessionId)
|
||||
.update(sessionData);
|
||||
} else {
|
||||
await db<Session>('sessions')
|
||||
.insert({id: sessionId, ...sessionData})
|
||||
.returning('*');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<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 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}`));
|
||||
}
|
Loading…
Reference in New Issue