Compare commits
5 Commits
679dd7a832
...
4ab739d681
Author | SHA1 | Date |
---|---|---|
Jill | 4ab739d681 | |
Jill | ef08ef020b | |
Jill | fcc7956b4d | |
Jill | cddcfee26e | |
Jill | 6d31321c14 |
|
@ -19,12 +19,12 @@ exports.up = async function(knex) {
|
||||||
|
|
||||||
// awfulllllllllllllll
|
// awfulllllllllllllll
|
||||||
const rows = await knex('counters').select('*');
|
const rows = await knex('counters').select('*');
|
||||||
await knex('counters_').insert(rows);
|
if (rows.length > 0) await knex('counters_').insert(rows);
|
||||||
|
|
||||||
await knex.schema
|
await knex.schema
|
||||||
.dropTable('counters')
|
.dropTable('counters')
|
||||||
.renameTable('counters_', 'counters');
|
.renameTable('counters_', 'counters');
|
||||||
|
|
||||||
await knex.schema
|
await knex.schema
|
||||||
.alterTable('counterUserLink', table => {
|
.alterTable('counterUserLink', table => {
|
||||||
table.integer('id').references('id').inTable('counters');
|
table.integer('id').references('id').inTable('counters');
|
||||||
|
|
|
@ -7,7 +7,7 @@ import * as log from './lib/log';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import { Command } from './types/index';
|
import { Command } from './types/index';
|
||||||
import { startServer } from './web';
|
import { startServer } from './web/web';
|
||||||
|
|
||||||
const bot = new Client({
|
const bot = new Client({
|
||||||
intents: [
|
intents: [
|
||||||
|
|
211
src/web.ts
211
src/web.ts
|
@ -1,211 +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 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 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 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)
|
|
||||||
.returning('*');
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,131 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { create } from 'express-handlebars';
|
||||||
|
import * as log from '../lib/log';
|
||||||
|
import { CustomItem, Counter, Session, CustomCraftingRecipe, 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';
|
||||||
|
|
||||||
|
async function getGuildInfo(bot: Client, id: string) {
|
||||||
|
const guild = await bot.guilds.cache.get(id);
|
||||||
|
if (!guild) return;
|
||||||
|
|
||||||
|
const items = await db<CustomItem>('customItems')
|
||||||
|
.where('guild', guild)
|
||||||
|
.count({count: '*'});
|
||||||
|
|
||||||
|
const counters = await db<Counter>('counters')
|
||||||
|
.where('guild', guild)
|
||||||
|
.count({count: '*'});
|
||||||
|
|
||||||
|
const recipes = await db<CustomCraftingRecipe>('customCraftingRecipes')
|
||||||
|
.where('guild', guild)
|
||||||
|
.count({count: '*'});
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: items[0].count as number,
|
||||||
|
counters: counters[0].count as number,
|
||||||
|
recipes: recipes[0].count as number,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startServer(bot: Client, port: number) {
|
||||||
|
const app = express();
|
||||||
|
const cdn = new CDN();
|
||||||
|
|
||||||
|
const hbs = create({
|
||||||
|
helpers: {
|
||||||
|
avatar: (id: string, hash: string) => (id && hash) ? cdn.avatar(id, hash, { size: 128 }) : '/assets/avatar.png',
|
||||||
|
icon: (id: string, hash: string) => (id && hash) ? cdn.icon(id, hash, { size: 128, forceStatic: true }) : '/assets/avatar.png',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.engine('handlebars', hbs.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 }) : '/assets/avatar.png',
|
||||||
|
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);
|
||||||
|
if (!user) return;
|
||||||
|
const guilds = await getGuilds(session);
|
||||||
|
if (!guilds) return;
|
||||||
|
|
||||||
|
//res.sendFile('profile/index.html', { root: 'static/' });
|
||||||
|
res.render('profile', {
|
||||||
|
user,
|
||||||
|
guilds: await Promise.all(
|
||||||
|
guilds.map(async guild =>
|
||||||
|
({...guild, jillo: await getGuildInfo(bot, guild.id)})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(express.static('static/'));
|
||||||
|
|
||||||
|
app.listen(port, () => log.info(`web interface listening on ${port}`));
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ body {
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
@ -133,7 +134,7 @@ a:hover {
|
||||||
#content {
|
#content {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
margin-bottom: 6rem;
|
margin-bottom: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,4 +294,60 @@ pre {
|
||||||
.note {
|
.note {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: var(--text-color-light);
|
color: var(--text-color-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0.5em;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.user .avatar {
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: 2rem;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guilds {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.guild {
|
||||||
|
order: 0;
|
||||||
|
display: flex;
|
||||||
|
width: 600px;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 3rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0.5rem;
|
||||||
|
background-color: var(--background-color-dark);
|
||||||
|
}
|
||||||
|
.guild.unavailable {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
.guild.unavailable .icon {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
.guild .icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
border-radius: 2rem;
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.guild .right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.guild .info {
|
||||||
|
color: var(--text-color-light);
|
||||||
}
|
}
|
|
@ -19,11 +19,10 @@
|
||||||
<div id="login" onclick="window.location = '/profile'">
|
<div id="login" onclick="window.location = '/profile'">
|
||||||
{{#if signedIn}}
|
{{#if signedIn}}
|
||||||
<div class="username">{{username}}</div>
|
<div class="username">{{username}}</div>
|
||||||
<img class="avatar" src="{{avatar}}" width="128" height="128">
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="username logged-out">log in</div>
|
<div class="username logged-out">log in</div>
|
||||||
<img class="avatar" src="/assets/avatar.png" width="128" height="128">
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
<img class="avatar" src="{{avatar}}" width="128" height="128">
|
||||||
</div>
|
</div>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<img src="/assets/jillo.png" width="150" height="200">
|
<img src="/assets/jillo.png" width="150" height="200">
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>jillo</title>
|
||||||
|
|
||||||
|
<meta name="theme-color" content="light dark">
|
||||||
|
<link href="/style.css" rel="stylesheet">
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Balsamiq+Sans&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
<div class="header">
|
||||||
|
<div class="bg"></div>
|
||||||
|
<div class="left">
|
||||||
|
<a href="/">jillo</a>
|
||||||
|
</div>
|
||||||
|
<div class="links">
|
||||||
|
<a href="https://discord.com/oauth2/authorize?client_id=898850107892596776&scope=bot" target="_blank" rel="noopener">invite</a>
|
||||||
|
·
|
||||||
|
<a href="https://git.oat.zone/dark-firepit/jillo-bot" target="_blank" rel="noopener">repo</a>
|
||||||
|
<img src="/assets/jillo_small.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{{body}}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<div class="user">
|
||||||
|
<img class="avatar" src="{{avatar user.id user.avatar}}" width="128" height="128">
|
||||||
|
<div class="username">Logged in as <b>{{user.global_name}}</b></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="guilds">
|
||||||
|
<h2>Guilds</h2>
|
||||||
|
{{#each guilds}}
|
||||||
|
{{#if jillo}}
|
||||||
|
<div class="guild">
|
||||||
|
<img src="{{icon id icon}}" width="128" height="128" class="icon">
|
||||||
|
<div class="right">
|
||||||
|
<div class="name">{{name}}</div>
|
||||||
|
<div class="info">{{jillo.counters}} counters · {{jillo.items}} items · {{jillo.recipes}} recipes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="guild unavailable">
|
||||||
|
<img src="{{icon id icon}}" width="128" height="128" class="icon">
|
||||||
|
<div class="right">
|
||||||
|
<div class="name">{{name}}</div>
|
||||||
|
<div class="info">Jillo is not in this server</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
Loading…
Reference in New Issue