Compare commits
5 Commits
60a3823b47
...
679dd7a832
Author | SHA1 | Date |
---|---|---|
Jill | 679dd7a832 | |
Jill | a487fc2f4c | |
Jill | c5d9954fcf | |
Jill | 0cd2b02282 | |
Jill | 249cf02490 |
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"token": "token",
|
||||
"sitePort": 15385,
|
||||
"siteURL": "https://localhost:15385"
|
||||
"siteURL": "http://localhost:15385",
|
||||
"clientId": "",
|
||||
"clientSecret": ""
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.up = function(knex) {
|
||||
return knex.schema
|
||||
.createTable('sessions', table => {
|
||||
table.string('id').notNullable();
|
||||
table.string('tokenType').notNullable();
|
||||
table.string('accessToken').notNullable();
|
||||
table.string('refreshToken').notNullable();
|
||||
table.timestamp('expiresAt').notNullable();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { import("knex").Knex } knex
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
exports.down = function(knex) {
|
||||
return knex.schema
|
||||
.dropTable('sessions');
|
||||
};
|
|
@ -12,22 +12,29 @@
|
|||
"author": "oatmealine",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@discordjs/core": "^1.1.1",
|
||||
"@discordjs/rest": "^2.2.0",
|
||||
"chalk": "^4.1.2",
|
||||
"d3-array": "^2.12.1",
|
||||
"discord.js": "^14.14.1",
|
||||
"express": "^4.18.2",
|
||||
"express-handlebars": "^7.1.2",
|
||||
"got": "^11.8.6",
|
||||
"knex": "^3.0.1",
|
||||
"outdent": "^0.8.0",
|
||||
"parse-color": "^1.0.0",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"random-seed": "^0.3.0",
|
||||
"sqlite3": "^5.1.6"
|
||||
"sqlite3": "^5.1.6",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"uid-safe": "^2.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3-array": "^3.2.1",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/parse-color": "^1.0.3",
|
||||
"@types/tough-cookie": "^4.0.5",
|
||||
"@types/uid-safe": "^2.1.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"discord-api-types": "^0.37.63",
|
||||
|
|
2081
pnpm-lock.yaml
2081
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -21,7 +21,7 @@ export default {
|
|||
.where('user', member.user.id);
|
||||
|
||||
// kind of stupid kind of awful
|
||||
const items = (await Promise.all(itemsList.map(async i => ({item: await getItem(i.item), quantity: i.quantity})))).filter(i => i.item);
|
||||
const items = (await Promise.all(itemsList.map(async i => ({item: await getItem(i.item), quantity: i.quantity})))).filter(i => i.item && i.quantity !== 0);
|
||||
|
||||
await interaction.followUp(
|
||||
`Your inventory:\n${items.length === 0 ? '_Your inventory is empty!_' : items.map(i => `- ${formatItems(i.item!, i.quantity)}\n_${i.item!.description}_`).join('\n')}`
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Client, GatewayIntentBits, Events, Collection, CommandInteraction, CommandInteractionOption, ApplicationCommandOptionType } from 'discord.js';
|
||||
import * as fs from 'fs';
|
||||
const { token, sitePort, siteURL } = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
||||
const { token, sitePort, siteURL, clientId, clientSecret } = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
|
||||
import * as path from 'path';
|
||||
import { initializeAnnouncements } from './lib/subscriptions';
|
||||
import * as log from './lib/log';
|
||||
|
@ -22,7 +22,7 @@ const bot = new Client({
|
|||
});
|
||||
|
||||
bot.config = {
|
||||
token, sitePort, siteURL
|
||||
token, sitePort, siteURL, clientId, clientSecret
|
||||
};
|
||||
|
||||
async function init() {
|
||||
|
|
|
@ -79,4 +79,11 @@ export interface CustomCraftingRecipeItem {
|
|||
item: number,
|
||||
quantity: number,
|
||||
type: 'input' | 'output' | 'requirement'
|
||||
}
|
||||
export interface Session {
|
||||
id: string,
|
||||
tokenType: string,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expiresAt: number,
|
||||
}
|
|
@ -316,7 +316,7 @@ export async function getItemQuantity(user: string, itemID: number): Promise<Ite
|
|||
};
|
||||
}
|
||||
|
||||
export async function giveItem(user: string, item: Item, quantity = 1) {
|
||||
export async function giveItem(user: string, item: Item, quantity = 1): Promise<ItemInventory> {
|
||||
const storedItem = await db<ItemInventory>('itemInventories')
|
||||
.where('user', user)
|
||||
.where('item', item.id)
|
||||
|
@ -324,6 +324,18 @@ export async function giveItem(user: string, item: Item, quantity = 1) {
|
|||
|
||||
let inv;
|
||||
if (storedItem) {
|
||||
if (storedItem.quantity + quantity === 0) {
|
||||
await db<ItemInventory>('itemInventories')
|
||||
.delete()
|
||||
.where('user', user)
|
||||
.where('item', item.id);
|
||||
return {
|
||||
user: user,
|
||||
item: item.id,
|
||||
quantity: 0
|
||||
};
|
||||
}
|
||||
|
||||
inv = await db<ItemInventory>('itemInventories')
|
||||
.update({
|
||||
quantity: db.raw('MIN(quantity + ?, ?)', [quantity, getMaxStack(item)])
|
||||
|
|
|
@ -11,7 +11,9 @@ export interface Command {
|
|||
export interface Config {
|
||||
token: string,
|
||||
sitePort: number,
|
||||
siteURL: string
|
||||
siteURL: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
}
|
||||
|
||||
declare module 'discord.js' {
|
||||
|
|
181
src/web.ts
181
src/web.ts
|
@ -1,13 +1,106 @@
|
|||
import express from 'express';
|
||||
import { engine } from 'express-handlebars';
|
||||
import * as log from './lib/log';
|
||||
import { CustomItem, db } from './lib/db';
|
||||
import { CustomItem, Session, db } from './lib/db';
|
||||
import { defaultItems } from './lib/rpg/items';
|
||||
import { Client } from 'discord.js';
|
||||
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.use(express.static('static/'));
|
||||
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;
|
||||
|
@ -32,5 +125,87 @@ export async function startServer(bot: Client, port: number) {
|
|||
});
|
||||
});
|
||||
|
||||
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}`));
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -11,6 +11,7 @@ body {
|
|||
font-family: 'Balsamiq Sans', sans-serif;
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
text-underline-offset: 3px;
|
||||
font-size: 16px;
|
||||
color-scheme: light dark;
|
||||
|
@ -18,6 +19,8 @@ body {
|
|||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:root {
|
||||
|
@ -52,7 +55,8 @@ a:hover {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
min-height: 100%;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
#main img {
|
||||
display: block;
|
||||
|
@ -95,6 +99,37 @@ a:hover {
|
|||
100% { transform: scale(1) rotate(0deg) }
|
||||
}
|
||||
|
||||
#login {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: fit-content;
|
||||
height: 2rem;
|
||||
border: 1px solid var(--text-color-light);
|
||||
border-radius: 2rem;
|
||||
align-items: center;
|
||||
padding: 0.25em 0.5em;
|
||||
margin: 0.5rem;
|
||||
gap: 0.5em;
|
||||
cursor: pointer;
|
||||
transition: 0.1s color, 0.1s background-color;
|
||||
font-size: 1.2rem;
|
||||
align-self: flex-end;
|
||||
}
|
||||
#login:hover {
|
||||
border-color: var(--accent-color);
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
#login .avatar {
|
||||
display: block;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 2rem;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
#login:not(:hover) .username.logged-out {
|
||||
color: var(--text-color-light);
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
|
|
|
@ -16,6 +16,15 @@
|
|||
<script src="/script.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="login" onclick="window.location = '/profile'">
|
||||
{{#if signedIn}}
|
||||
<div class="username">{{username}}</div>
|
||||
<img class="avatar" src="{{avatar}}" width="128" height="128">
|
||||
{{else}}
|
||||
<div class="username logged-out">log in</div>
|
||||
<img class="avatar" src="/assets/avatar.png" width="128" height="128">
|
||||
{{/if}}
|
||||
</div>
|
||||
<div id="main">
|
||||
<img src="/assets/jillo.png" width="150" height="200">
|
||||
<h1>jillo!</h1>
|
Loading…
Reference in New Issue