diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f9275b9 --- /dev/null +++ b/.env.example @@ -0,0 +1,39 @@ +# Copy this file to `.env.example` before editing + +### IMPORTANT: ### +# After changing `BOT_PREFIX` or `PUBLIC_API_URL`, +# you need to rebuild the web container: +# "docker-compose build web" + +# Set a secure password for your database. +# Keep in mind that you can't easily change this +# after initializing the database. +DB_PASS= + +# Your bot account's token. +BOT_TOKEN= + +# The default prefix, can be overridden by server owners. +# Leave empty to keep default ("/") +BOT_PREFIX= + +# Private token used by the bot to communicate +# with the API. Keep this private! +INTERNAL_API_TOKEN= + +# Comma separated list of user IDs. +# Be careful with the people you add here - +# They will be able to use /eval and /shell, +# which gives them access to your server! +BOT_OWNERS= + +# Optional: A Discord webhook URL which will be +# used to log certain events (bot started, etc). +LOG_WEBHOOK= + +# The URL from which your API and Web app are +# publicly reachable. Do not add a trailing +# slash to the URLs. + +PUBLIC_API_URL=http://localhost:9000 +PUBLIC_WEB_URL=http://localhost:8080 diff --git a/.gitignore b/.gitignore index a58e32a..b215548 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ node_modules dist .env yarn-error.log +docker-compose.yml +/db +.vercel + +# For stuff like docker mounts +/private diff --git a/Dockerfile b/api/Dockerfile similarity index 93% rename from Dockerfile rename to api/Dockerfile index 6b4f85d..e29635c 100644 --- a/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16 AS build +FROM node:16 as build WORKDIR /build/ COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..87ec9b3 --- /dev/null +++ b/api/package.json @@ -0,0 +1,28 @@ +{ + "name": "api", + "version": "1.0.0", + "description": "", + "exports": "./index", + "type": "module", + "scripts": { + "build": "rm -rf dist && tsc", + "start": "node --experimental-specifier-resolution=node dist/index", + "dev": "yarn build && yarn start" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@types/express": "^4.17.13", + "@types/monk": "^6.0.0", + "@types/ws": "^8.2.2", + "dotenv": "^14.2.0", + "express": "^4.17.2", + "log75": "^2.2.0", + "monk": "^7.3.4", + "ws": "^8.4.2" + }, + "devDependencies": { + "typescript": "^4.5.5" + } +} diff --git a/api/src/db.ts b/api/src/db.ts new file mode 100644 index 0000000..124ddcc --- /dev/null +++ b/api/src/db.ts @@ -0,0 +1,29 @@ +import Monk, { IMonkManager } from 'monk'; +import { logger } from '.'; + +export default (): IMonkManager => { + let dburl = getDBUrl(); + let db = Monk(dburl); + return db; +}; + +// Checks if all required env vars were supplied, and returns the mongo db URL +function getDBUrl() { + let env = process.env; + if (env['DB_URL']) return env['DB_URL']; + + if (!env['DB_HOST']) { + logger.error(`Environment variable 'DB_HOST' not set, unable to connect to database`); + logger.error(`Specify either 'DB_URL' or 'DB_HOST', 'DB_USERNAME', 'DB_PASS' and 'DB_NAME'`); + throw 'Missing environment variables'; + } + + // mongodb://username:password@hostname:port/dbname + let dburl = 'mongodb://'; + if (env['DB_USERNAME']) dburl += env['DB_USERNAME']; + if (env['DB_PASS']) dburl += `:${env['DB_PASS']}`; + dburl += `${process.env['DB_USERNAME'] ? '@' : ''}${env['DB_HOST']}`; // DB_HOST is assumed to contain the port + dburl += `/${env['DB_NAME'] ?? 'automod'}`; + + return dburl; +} diff --git a/api/src/index.ts b/api/src/index.ts new file mode 100644 index 0000000..dfffa4e --- /dev/null +++ b/api/src/index.ts @@ -0,0 +1,42 @@ +import { config } from 'dotenv'; +import Express from "express"; +import Log75, { LogLevel } from 'log75'; +import buildDBClient from './db'; + +config(); + +const PORT = Number(process.env.API_PORT || 9000); +const DEBUG = process.env.NODE_ENV != 'production'; +const SESSION_LIFETIME = 1000 * 60 * 60 * 24 * 7; + +const logger: Log75 = new (Log75 as any).default(DEBUG ? LogLevel.Debug : LogLevel.Standard); +const db = buildDBClient(); +const app = Express(); + +app.set('trust proxy', true); +app.use(Express.json()); + +export { logger, app, db, PORT, SESSION_LIFETIME } + +(async () => { + const promises = [ + import('./middlewares/log'), + import('./middlewares/updateTokenExpiry'), + import('./middlewares/cors'), + import('./middlewares/ratelimit'), + + import('./routes/internal/ws'), + import('./routes/root'), + import('./routes/login'), + import('./routes/dash/servers'), + import('./routes/dash/server'), + import('./routes/dash/server-automod'), + ]; + + for (const p of promises) await p; + + + logger.done('All routes and middlewares loaded'); +})(); + +import('./server'); diff --git a/api/src/middlewares/cors.ts b/api/src/middlewares/cors.ts new file mode 100644 index 0000000..13972ab --- /dev/null +++ b/api/src/middlewares/cors.ts @@ -0,0 +1,9 @@ +import { Request, Response } from "express"; +import { app } from ".."; + +app.use('*', (req: Request, res: Response, next: () => void) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, x-auth-user, x-auth-token'); + res.header('Access-Control-Allow-Methods', '*'); + next(); +}); diff --git a/api/src/middlewares/log.ts b/api/src/middlewares/log.ts new file mode 100644 index 0000000..6d912c1 --- /dev/null +++ b/api/src/middlewares/log.ts @@ -0,0 +1,7 @@ +import { Request, Response } from "express"; +import { app, logger } from ".."; + +app.use('*', (req: Request, res: Response, next: () => void) => { + logger.debug(`${req.method} ${req.url}`); + next(); +}); diff --git a/api/src/middlewares/ratelimit.ts b/api/src/middlewares/ratelimit.ts new file mode 100644 index 0000000..a3b4dff --- /dev/null +++ b/api/src/middlewares/ratelimit.ts @@ -0,0 +1,109 @@ +import { Request, Response } from "express"; +import { FindOneResult } from "monk"; +import { app, db, logger } from ".."; + +const ratelimits = db.get('ratelimits'); + +type RateLimitObject = { + ip: string, + requests: { route: string, time: number }[], + lastActivity: number, +} + +// Might use redis here later, idk +// I am also aware that there's better ways to do this + +class RateLimiter { + route: string; + limit: number; + timeframe: number; + + constructor(route: string, limits: { limit: number, timeframe: number }) { + this.route = route; + this.limit = limits.limit; + this.timeframe = limits.timeframe; + } + + async execute(req: Request, res: Response, next: () => void) { + try { + const ip = req.ip; + const now = Date.now(); + + const entry: FindOneResult = await ratelimits.findOne({ ip }); + if (!entry) { + logger.debug('Ratelimiter: Request from new IP address, creating new document'); + next(); + await ratelimits.insert({ + ip, + lastActivity: now, + requests: [{ route: this.route, time: now }], + }); + return; + } + + const reqs = entry.requests.filter( + r => r.route == this.route && r.time > now - (this.timeframe * 1000) + ); + + if (reqs.length >= this.limit) { + logger.debug(`Ratelimiter: IP address exceeded ratelimit for ${this.route} [${this.limit}/${this.timeframe}]`); + res + .status(429) + .send({ + error: 'You are being rate limited.', + limit: this.limit, + timeframe: this.timeframe, + }); + } else next(); + + // Can't put a $push and $pull into the same query + await Promise.all([ + ratelimits.update({ ip }, { + $push: { + requests: { route: this.route, time: now } + }, + $set: { + lastActivity: now + } + }), + ratelimits.update({ ip }, { + $pull: { + requests: { + route: this.route, + time: { + $lt: now - (this.timeframe * 1000) + } + } + } + }), + ]); + } catch(e) { console.error(e) } + } +} + +app.use('*', (...args) => (new RateLimiter('*', { limit: 20, timeframe: 1 })).execute(...args)); + +// Delete all documents where the last +// activity was more than 24 hours ago. +// This ensures that we don't store +// personally identifying data for longer +// than required. + +const cleanDocuments = async () => { + try { + logger.info('Ratelimiter: Deleting old documents'); + + const { deletedCount } = await ratelimits.remove({ + lastActivity: { $lt: Date.now() - 1000 * 60 * 60 * 24 } + }, { multi: true }); + + logger.done(`Ratelimiter: Deleted ${deletedCount ?? '??'} documents.`); + } catch(e) { + console.error(e); + } +} + +setTimeout(cleanDocuments, 1000 * 10); +setInterval(cleanDocuments, 10000 * 60 * 60); + +export { RateLimiter } diff --git a/api/src/middlewares/updateTokenExpiry.ts b/api/src/middlewares/updateTokenExpiry.ts new file mode 100644 index 0000000..35fe74f --- /dev/null +++ b/api/src/middlewares/updateTokenExpiry.ts @@ -0,0 +1,19 @@ +import { Request, Response } from "express"; +import { FindOneResult } from "monk"; +import { app, db, SESSION_LIFETIME } from ".."; + +app.use('*', async (req: Request, res: Response, next: () => void) => { + next(); + + const user = req.header('x-auth-user'); + const token = req.header('x-auth-token'); + + if (!user || !token) return; + + try { + const session: FindOneResult = await db.get('sessions').findOne({ user, token, expires: { $gt: Date.now() } }); + if (session) { + await db.get('sessions').update({ _id: session._id }, { $set: { expires: Date.now() + SESSION_LIFETIME } }); + } + } catch(e) { console.error(e) } +}); diff --git a/api/src/routes/dash/server-automod.ts b/api/src/routes/dash/server-automod.ts new file mode 100644 index 0000000..2a1f6f8 --- /dev/null +++ b/api/src/routes/dash/server-automod.ts @@ -0,0 +1,92 @@ +import { app, db } from '../..'; +import { Request, Response } from 'express'; +import { badRequest, isAuthenticated, unauthorized } from '../../utils'; +import { botReq } from '../internal/ws'; +import { FindOneResult } from 'monk'; + +type AntispamRule = { + id: string; + max_msg: number; + timeframe: number; + action: 0|1|2|3|4; + channels: string[] | null; + message: string | null; +} + +app.get('/dash/server/:server/automod', async (req: Request, res: Response) => { + const user = await isAuthenticated(req, res, true); + if (!user) return; + + const { server } = req.params; + if (!server || typeof server != 'string') return badRequest(res); + + const response = await botReq('getUserServerDetails', { user, server }); + if (!response.success) { + return res.status(response.statusCode ?? 500).send({ error: response.error }); + } + + if (!response.server) return res.status(404).send({ error: 'Server not found' }); + + const permissionLevel: 0|1|2|3 = response.perms; + if (permissionLevel < 1) return unauthorized(res, `Only moderators and bot managers may view this.`); + + const serverConfig: FindOneResult = await db.get('servers').findOne({ id: server }); + + const result = { + antispam: (serverConfig.automodSettings?.spam as AntispamRule[]|undefined) + ?.map(r => ({ // Removing unwanted fields from response + action: r.action, + channels: r.channels, + id: r.id, + max_msg: r.max_msg, + message: r.message, + timeframe: r.timeframe, + } as AntispamRule)) + ?? [] + } + + res.send(result); +}); + +app.patch('/dash/server/:server/automod/:ruleid', async (req: Request, res: Response) => { + const user = await isAuthenticated(req, res, true); + if (!user) return; + + const { server, ruleid } = req.params; + const body = req.body; + if (!server || !ruleid) return badRequest(res); + + const response = await botReq('getUserServerDetails', { user, server }); + if (!response.success) { + return res.status(response.statusCode ?? 500).send({ error: response.error }); + } + + if (!response.server) return res.status(404).send({ error: 'Server not found' }); + + const permissionLevel: 0|1|2|3 = response.perms; + if (permissionLevel < 2) return unauthorized(res, `Only bot managers can manage moderation rules.`); + + const serverConfig: FindOneResult = await db.get('servers').findOne({ id: server }); + const antiSpamRules: AntispamRule[] = serverConfig.automodSettings?.spam ?? []; + + const rule = antiSpamRules.find(r => r.id == ruleid); + if (!rule) return res.status(404).send({ error: 'No rule with this ID could be found.' }); + + await db.get('servers').update({ + id: server + }, { + $set: { + "automodSettings.spam.$[rulefilter]": { + ...rule, + action: body.action ?? rule.action, + channels: body.channels ?? rule.channels, + message: body.message ?? rule.message, + max_msg: body.max_msg ?? rule.max_msg, + timeframe: body.timeframe ?? rule.timeframe, + + } as AntispamRule + } + }, { arrayFilters: [ { "rulefilter.id": ruleid } ] }); + + return res.send({ success: true }); +}); diff --git a/api/src/routes/dash/server.ts b/api/src/routes/dash/server.ts new file mode 100644 index 0000000..85c264a --- /dev/null +++ b/api/src/routes/dash/server.ts @@ -0,0 +1,195 @@ +import { app, db } from '../..'; +import { Request, Response } from 'express'; +import { badRequest, getPermissionLevel, isAuthenticated, unauthorized } from '../../utils'; +import { botReq } from '../internal/ws'; + +type User = { id: string, username?: string, avatarURL?: string } +type Channel = { id: string, name: string, icon?: string, type: 'VOICE'|'TEXT', nsfw: boolean } + +type ServerDetails = { + id: string, + perms: 0|1|2|3, + name: string, + description?: string, + iconURL?: string, + bannerURL?: string, + serverConfig: any, + users: User[], + channels: Channel[], +} + +app.get('/dash/server/:server', async (req: Request, res: Response) => { + const user = await isAuthenticated(req, res, true); + if (!user) return; + + const { server } = req.params; + if (!server || typeof server != 'string') return badRequest(res); + + const response = await botReq('getUserServerDetails', { user, server }); + if (!response.success) { + return res.status(response.statusCode ?? 500).send({ error: response.error }); + } + + if (!response.server) return res.status(404).send({ error: 'Not found' }); + + const s: ServerDetails = response.server; + res.send({ server: s }); +}); + +app.put('/dash/server/:server/:option', async (req: Request, res: Response) => { + try { + const user = await isAuthenticated(req, res, true); + if (!user) return; + + const { server } = req.params; + const { item } = req.body; + if (!server || typeof server != 'string') return badRequest(res); + + const permissionLevelRes = await getPermissionLevel(user, server); + if (!permissionLevelRes.success) + return res.status(permissionLevelRes.statusCode || 500).send({ error: permissionLevelRes.error }); + + const servers = db.get('servers'); + const permissionLevel: 0|1|2|3 = permissionLevelRes.level; + const settings = await servers.findOne({ id: server }); + + switch(req.params.option) { + case 'managers': { + if (!item || typeof item != 'string') return badRequest(res); + if (permissionLevel < 3) return res.status(403).send({ error: 'You are not allowed to add other bot managers.' }); + + const userRes = await botReq('getUser', { user: item }); + if (!userRes.success) { + return res.status(404).send({ error: 'User could not be found' }); + } + + if (settings.botManagers?.includes(userRes.user.id) === true) { + return res.status(400).send({ error: 'This user is already manager' }); + } + + const newManagers = [ ...(settings.botManagers ?? []), userRes.user.id ]; + await servers.update({ id: server }, { $set: { botManagers: newManagers } }); + res.send({ + success: true, + managers: newManagers, + users: [ userRes.user ], + }); + return; + } + + case 'mods': { + if (!item || typeof item != 'string') return badRequest(res); + if (permissionLevel < 2) return res.status(403).send({ error: 'You are not allowed to add other moderators.' }); + + const userRes = await botReq('getUser', { user: item }); + if (!userRes.success) { + return res.status(404).send({ error: 'User could not be found' }); + } + + if (settings.moderators?.includes(userRes.user.id) === true) { + return res.status(400).send({ error: 'This user is already moderator' }); + } + + const newMods = [ ...(settings.moderators ?? []), userRes.user.id ]; + await servers.update({ id: server }, { $set: { moderators: newMods } }); + res.send({ + success: true, + mods: newMods, + users: [ userRes.user ], + }); + return; + } + + case 'config': { + function validateField(field: string, type: string[], level: 0|1|2|3): boolean { + if (permissionLevel < level) { + res.status(403).send({ error: `You are not authorized to change '${field}'` }); + return false; + } + + if (req.body?.[field] != undefined && !type.includes(typeof req.body?.[field])) { + res.status(400).send({ error: `Field '${field}' needs to be of type ${type} or null` }); + return false; + } + + return true; + } + + type RequestBody = { + prefix?: string, + spaceAfterPrefix?: boolean, + } + + if (!validateField('prefix', ['string'], 2) || + !validateField('spaceAfterPrefix', ['boolean'], 2) + ) return; + + const body: RequestBody = req.body; + + await db.get('servers').update({ id: server }, { + $set: JSON.parse(JSON.stringify({ // Get rid of undefined fields + prefix: body.prefix == '' ? null : body.prefix, + spaceAfterPrefix: body.spaceAfterPrefix, + })), + }); + + return res.send({ success: true }); + } + + default: return badRequest(res); + } + } catch(e: any) { + console.error(e); + res.status(500).send({ error: e }); + } +}); + +app.delete('/dash/server/:server/:option/:target', async (req: Request, res: Response) => { + const user = await isAuthenticated(req, res, true); + if (!user) return unauthorized(res); + + const { server, target, option } = req.params; + if (!server || typeof server != 'string' || !target || typeof target != 'string') return badRequest(res); + + const permissionLevelRes = await getPermissionLevel(user, server); + if (!permissionLevelRes.success) + return res.status(permissionLevelRes.statusCode || 500).send({ error: permissionLevelRes.error }); + + const servers = db.get('servers'); + const permissionLevel: 0|1|2|3 = permissionLevelRes.level; + const settings = await servers.findOne({ id: server }); + + switch(option) { + case 'managers': { + if (permissionLevel < 3) return res.status(403).send({ error: 'You are not allowed to remove bot managers.' }); + + if (!settings.botManagers?.includes(target)) { + return res.status(400).send({ error: 'This user is not manager' }); + } + + const newManagers = (settings.botManagers ?? []).filter((i: string) => i != target); + await servers.update({ id: server }, { $set: { botManagers: newManagers } }); + res.send({ + success: true, + managers: newManagers, + }); + return; + } + case 'mods': { + if (permissionLevel < 2) return res.status(403).send({ error: 'You are not allowed to remove moderators.' }); + + if (!settings.moderators?.includes(target)) { + return res.status(400).send({ error: 'This user is not moderator' }); + } + + const newMods = (settings.moderators ?? []).filter((i: string) => i != target); + await servers.update({ id: server }, { $set: { moderators: newMods } }); + res.send({ + success: true, + mods: newMods, + }); + return; + } + default: return badRequest(res); + } +}); diff --git a/api/src/routes/dash/servers.ts b/api/src/routes/dash/servers.ts new file mode 100644 index 0000000..fd36317 --- /dev/null +++ b/api/src/routes/dash/servers.ts @@ -0,0 +1,21 @@ +import { app } from '../..'; +import { Request, Response } from 'express'; +import { isAuthenticated, unauthorized } from '../../utils'; +import { botReq } from '../internal/ws'; + +type Server = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string } + +app.get('/dash/servers', async (req: Request, res: Response) => { + const user = await isAuthenticated(req); + if (!user) return unauthorized(res); + + const response = await botReq('getUserServers', { user }); + if (!response.success) { + return res.status(response.statusCode ?? 500).send({ error: response.error }); + } + + if (!response.servers) return res.status(404).send({ error: 'Not found' }); + + const servers: Server[] = response.servers; + res.send({ servers }); +}); diff --git a/api/src/routes/internal/ws.ts b/api/src/routes/internal/ws.ts new file mode 100644 index 0000000..b236163 --- /dev/null +++ b/api/src/routes/internal/ws.ts @@ -0,0 +1,87 @@ +/** + * Provides a WebSocket the bot can connect to. + * (IPC on crack) + */ + +import { WebSocketServer, WebSocket } from 'ws'; +import { EventEmitter } from 'events'; +import { logger } from "../.."; +import server from '../../server'; + +if (!process.env.BOT_API_TOKEN) { + logger.error(`$BOT_API_TOKEN is not set. This token is ` + + `required for the bot to communicate with the API.`); + process.exit(1); +} + +const { BOT_API_TOKEN } = process.env; +const wsServer = new WebSocketServer({ noServer: true }); +const botWS = new EventEmitter(); +const sockets: WebSocket[] = []; + +wsServer.on('connection', (sock) => { + sockets.push(sock); + + sock.once('close', () => { + logger.debug('WS closed'); + const i = sockets.findIndex(s => s == sock); + sockets.splice(i, 1); + }); + + sock.on('message', (msg) => { + const jsonBody = JSON.parse(msg.toString()); + logger.debug(`[WS] [<] ${msg.toString()}`); + botWS.emit('message', jsonBody); + if (jsonBody.data && jsonBody.type) { + botWS.emit(jsonBody.type, jsonBody.data); + } + }); +}); + +server.on('upgrade', (req, socket, head) => { + logger.debug(`WS Upgrade ${req.url}`); + + switch(req.url) { + case '/internal/ws': + if (req.headers['authorization'] !== BOT_API_TOKEN) { + logger.debug('WS unauthorized'); + head.write(JSON.stringify({ error: 'Not authenticated' }, null, 4)); + socket.end(); + } else { + wsServer.handleUpgrade(req, socket, head, (sock) => { + wsServer.emit('connection', sock, req); + }); + } + break; + default: + head.write(JSON.stringify({ error: 'Cannot open WebSocket on this endpoint' }, null, 4)); + socket.end(); + } +}); + +function sendBotWS(msg: { [key: string]: any }) { + const socks = sockets.filter(sock => sock.readyState == sock.OPEN); + logger.debug(`[WS] [>] [${socks.length}] ${JSON.stringify(msg)}`); + socks.forEach(sock => sock.send(JSON.stringify(msg))); +} + +type botReqRes = { success: false, error: string, statusCode?: number } | { success: true, [key: string]: any } +function botReq(type: string, data?: { [key: string]: any }): Promise { + return new Promise((resolve, reject) => { + const nonce = `${Date.now()}.${Math.round(Math.random() * 10000000)}`; + if (sockets.length == 0) return resolve({ success: false, error: 'Unable to communicate with bot' }); + sendBotWS({ nonce, type, data }); + botWS.once(`response:${nonce}`, (data: string|Object) => { + try { + const d = typeof data == 'string' ? JSON.parse(data || '{}') : data; + if (d.success == undefined) d.success = true; + if (d.success == false && !d.error) d.error = 'Unknown error'; + resolve(d); + } catch(e) { reject(e) } + }); + }); +} + +//setInterval(() => botReq('test', { "sus": true }), 1000); + +export { botWS, sendBotWS, botReq } diff --git a/api/src/routes/login.ts b/api/src/routes/login.ts new file mode 100644 index 0000000..bb60002 --- /dev/null +++ b/api/src/routes/login.ts @@ -0,0 +1,68 @@ +import crypto from 'crypto'; +import { app, SESSION_LIFETIME } from '..'; +import { Request, Response } from 'express'; +import { botReq } from './internal/ws'; +import { db } from '..'; +import { FindOneResult } from 'monk'; +import { badRequest } from '../utils'; +import { RateLimiter } from '../middlewares/ratelimit'; + +class BeginReqBody { + user: string; +} +class CompleteReqBody { + user: string; + nonce: string; + code: string; +} + +const beginRatelimiter = new RateLimiter('/login/begin', { limit: 10, timeframe: 300 }); +const completeRatelimiter = new RateLimiter('/login/complete', { limit: 5, timeframe: 30 }); + +app.post('/login/begin', (...args) => beginRatelimiter.execute(...args), async (req: Request, res: Response) => { + const body = req.body as BeginReqBody; + if (!body.user || typeof body.user != 'string') return badRequest(res); + + const r = await botReq('requestLogin', { user: body.user.toLowerCase() }); + + if (!r.success) return res.status(r.statusCode ?? 500).send(JSON.stringify({ error: r.error }, null, 4)); + + res.status(200).send({ success: true, nonce: r.nonce, code: r.code, uid: r.uid }); +}); + +app.post('/login/complete', (...args) => completeRatelimiter.execute(...args), async (req: Request, res: Response) => { + const body = req.body as CompleteReqBody; + if ((!body.user || typeof body.user != 'string') || + (!body.nonce || typeof body.nonce != 'string') || + (!body.code || typeof body.code != 'string')) return badRequest(res); + + const loginAttempt: FindOneResult = await db.get('pending_logins').findOne({ + code: body.code, + user: body.user, + nonce: body.nonce, + exchanged: false, + invalid: false, + }); + + if (!loginAttempt) return res.status(404).send({ error: 'The provided login info could not be found.' }); + + if (!loginAttempt.confirmed) { + return res.status(400).send({ error: "This code is not yet valid." }); + } + + const sessionToken = crypto.randomBytes(48).toString('base64').replace(/=/g, ''); + + + await Promise.all([ + db.get('sessions').insert({ + user: body.user.toUpperCase(), + token: sessionToken, + nonce: body.nonce, + invalid: false, + expires: Date.now() + SESSION_LIFETIME, + }), + db.get('pending_logins').update({ _id: loginAttempt._id }, { $set: { exchanged: true } }), + ]); + + res.status(200).send({ success: true, user: body.user.toUpperCase(), token: sessionToken }); +}); diff --git a/api/src/routes/root.ts b/api/src/routes/root.ts new file mode 100644 index 0000000..9707409 --- /dev/null +++ b/api/src/routes/root.ts @@ -0,0 +1,11 @@ +import { app } from '..'; +import { Request, Response } from 'express'; +import { getSessionInfo, isAuthenticated } from '../utils'; + +app.get('/', async (req: Request, res: Response) => { + const isAuthed = await isAuthenticated(req); + res.send({ + authenticated: isAuthed, + sessionInfo: isAuthed ? await getSessionInfo(req.header('x-auth-user')!, req.header('x-auth-token')!) : {}, + }); +}); \ No newline at end of file diff --git a/api/src/server.ts b/api/src/server.ts new file mode 100644 index 0000000..5f2e17e --- /dev/null +++ b/api/src/server.ts @@ -0,0 +1,5 @@ +import { app, logger, PORT } from "."; + +const server = app.listen(PORT, () => logger.info(`Listening on port ${PORT}`)); + +export default server; diff --git a/api/src/utils.ts b/api/src/utils.ts new file mode 100644 index 0000000..1c278fa --- /dev/null +++ b/api/src/utils.ts @@ -0,0 +1,52 @@ +import { Request, Response } from "express"; +import { FindOneResult } from "monk"; +import { db } from "."; +import { botReq } from "./routes/internal/ws"; + +class Session { + user: string; + token: string; + nonce: string; + expires: number; + invalid: boolean; +} + +/** + * + * @param req + * @returns false if not authenticated, otherwise the (Revolt) user ID + */ +async function isAuthenticated(req: Request, res?: Response, send401?: boolean): Promise { + const user = req.header('x-auth-user'); + const token = req.header('x-auth-token'); + + if (!user || !token) return false; + + const info = await getSessionInfo(user, token); + if (res && send401 && !info.valid) { + res.status(401).send({ error: 'Unauthorized' }); + } + return info.valid ? user : false; +} + +type SessionInfo = { exists: boolean, valid: boolean, nonce?: string } + +async function getSessionInfo(user: string, token: string): Promise { + const session: FindOneResult = await db.get('sessions').findOne({ user, token }); + + return { exists: !!session, valid: !!(session && !session.invalid && session.expires > Date.now()), nonce: session?.nonce } +} + +function badRequest(res: Response, infoText?: string) { + res.status(400).send(JSON.stringify({ "error": "Invalid request body", "info": infoText || undefined }, null, 4)); +} + +function unauthorized(res: Response, infoText?: string) { + res.status(401).send(JSON.stringify({ "error": "Unauthorized", "info": infoText || undefined }, null, 4)); +} + +async function getPermissionLevel(user: string, server: string) { + return await botReq('getPermissionLevel', { user, server }); +} + +export { isAuthenticated, getSessionInfo, badRequest, unauthorized, getPermissionLevel } diff --git a/tsconfig.json b/api/tsconfig.json similarity index 100% rename from tsconfig.json rename to api/tsconfig.json diff --git a/api/yarn.lock b/api/yarn.lock new file mode 100644 index 0000000..fb6ac4c --- /dev/null +++ b/api/yarn.lock @@ -0,0 +1,665 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bson@*": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.2.0.tgz#a2f71e933ff54b2c3bf267b67fa221e295a33337" + integrity sha512-ELCPqAdroMdcuxqwMgUpifQyRoTpyYCNr1V9xKyF40VsBobsj+BbWNRvwGchMgBPGqkw655ypkjj2MEF5ywVwg== + dependencies: + bson "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.18": + version "4.17.28" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" + integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.13": + version "4.17.13" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" + integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/mongodb@^3.5.25": + version "3.6.20" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.20.tgz#b7c5c580644f6364002b649af1c06c3c0454e1d2" + integrity sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ== + dependencies: + "@types/bson" "*" + "@types/node" "*" + +"@types/monk@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/monk/-/monk-6.0.0.tgz#236750988e24d92c674529a81b9a296f8bbc3763" + integrity sha512-9qy4Gva0uVgaQsDqlcWD+XOXUmgInPAoxyyoN8uFUTjNFvswyCH1hwpnYuh2MVr60ekZgYiquWEjBYvfYfE1Jw== + dependencies: + monk "*" + +"@types/node@*": + version "17.0.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.10.tgz#616f16e9d3a2a3d618136b1be244315d95bd7cab" + integrity sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/serve-static@*": + version "1.13.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/ws@^8.2.2": + version "8.2.2" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21" + integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== + dependencies: + "@types/node" "*" + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bl@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" + integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +body-parser@1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.1.tgz#1499abbaa9274af3ecc9f6f10396c995943e31d4" + integrity sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA== + dependencies: + bytes "3.1.1" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.8.1" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.9.6" + raw-body "2.4.2" + type-is "~1.6.18" + +bson@*: + version "4.6.1" + resolved "https://registry.yarnpkg.com/bson/-/bson-4.6.1.tgz#2b5da517539bb0f7f3ffb54ac70a384ca899641c" + integrity sha512-I1LQ7Hz5zgwR4QquilLNZwbhPw0Apx7i7X9kGMBTsqPdml/03Q9NBtD9nt/19ahjlphktQImrnderxqpzeVDjw== + dependencies: + buffer "^5.6.0" + +bson@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.6.tgz#fb819be9a60cd677e0853aee4ca712a785d6618a" + integrity sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg== + +buffer@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +bytes@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" + integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +debug@*: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +denque@^1.4.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +dotenv@^14.2.0: + version "14.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-14.2.0.tgz#7e77fd5dd6cff5942c4496e1acf2d0f37a9e67aa" + integrity sha512-05POuPJyPpO6jqzTNweQFfAyMSD4qa4lvsMOWyTRTdpHKy6nnnN+IYWaXF+lHivhBH/ufDKlR4IWCAN3oPnHuw== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.17.2: + version "4.17.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.2.tgz#c18369f265297319beed4e5558753cc8c1364cb3" + integrity sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.4.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.9.6" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.17.2" + serve-static "1.14.2" + setprototypeof "1.2.0" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +http-errors@1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" + integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +log75@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log75/-/log75-2.2.0.tgz#2505159b450c3cd1b4e863a837e883a7ecbaa2a1" + integrity sha512-B4aMjdqCzIfJy7Fm2fZeVoB0WHK7O3NxZnFz99603wg3rItZ2os2kfeOFlh5bHNOsKDSRF2lbfGfYL3O7qgETg== + dependencies: + ansi-colors "^4.1.1" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.51.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== + +mime-types@~2.1.24: + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== + dependencies: + mime-db "1.51.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mongodb@^3.2.3: + version "3.7.3" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.7.3.tgz#b7949cfd0adc4cc7d32d3f2034214d4475f175a5" + integrity sha512-Psm+g3/wHXhjBEktkxXsFMZvd3nemI0r3IPsE0bU+4//PnvNWKkzhZcEsbPcYiWqe8XqXJJEg4Tgtr7Raw67Yw== + dependencies: + bl "^2.2.1" + bson "^1.1.4" + denque "^1.4.1" + optional-require "^1.1.8" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +monk-middleware-cast-ids@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/monk-middleware-cast-ids/-/monk-middleware-cast-ids-0.2.1.tgz#40c40e5a6cb33ccedc289220943275ee8861c529" + integrity sha1-QMQOWmyzPM7cKJIglDJ17ohhxSk= + +monk-middleware-fields@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/monk-middleware-fields/-/monk-middleware-fields-0.2.0.tgz#ff637af35f5948879ccb2be15a91360911bea6c1" + integrity sha1-/2N6819ZSIecyyvhWpE2CRG+psE= + +monk-middleware-handle-callback@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/monk-middleware-handle-callback/-/monk-middleware-handle-callback-0.2.2.tgz#47de6cc1248726c72a2be0c81bc4e68310c32146" + integrity sha512-5hBynb7asZ2uw9XVze7C3XH0zXT51yFDvYydk/5HnWWzh2NLglDSiKDcX0yLKPHzFgiq+5Z4Laq5fFVnFsmm8w== + +monk-middleware-options@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/monk-middleware-options/-/monk-middleware-options-0.2.1.tgz#58dae1c518d46636ebdff506fadfc773bb442886" + integrity sha1-WNrhxRjUZjbr3/UG+t/Hc7tEKIY= + +monk-middleware-query@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/monk-middleware-query/-/monk-middleware-query-0.2.0.tgz#a926c677d4a5620c62151b0a56d0c0c151675874" + integrity sha1-qSbGd9SlYgxiFRsKVtDAwVFnWHQ= + +monk-middleware-wait-for-connection@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/monk-middleware-wait-for-connection/-/monk-middleware-wait-for-connection-0.2.0.tgz#312958d30e588b57d09754dd7c97b4843316835a" + integrity sha1-MSlY0w5Yi1fQl1TdfJe0hDMWg1o= + +monk@*, monk@^7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/monk/-/monk-7.3.4.tgz#50ccd7daebb4c16ace58d45b2c28c29112f8a85e" + integrity sha512-PkPNiElwroVyKQj01usyziOvwiKYBUVSq7YU1FB4KFr0J3v0GeXW0TebYsLR4u33WB8JGqPiAcuzDspfdujqQg== + dependencies: + "@types/mongodb" "^3.5.25" + debug "*" + mongodb "^3.2.3" + monk-middleware-cast-ids "^0.2.1" + monk-middleware-fields "^0.2.0" + monk-middleware-handle-callback "^0.2.0" + monk-middleware-options "^0.2.1" + monk-middleware-query "^0.2.0" + monk-middleware-wait-for-connection "^0.2.0" + object-assign "^4.1.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +optional-require@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.1.8.tgz#16364d76261b75d964c482b2406cb824d8ec44b7" + integrity sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA== + dependencies: + require-at "^1.0.6" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@6.9.6: + version "6.9.6" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" + integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" + integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== + dependencies: + bytes "3.1.1" + http-errors "1.8.1" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.3.5: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +require-at@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a" + integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g== + +safe-buffer@5.2.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saslprep@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + +send@0.17.2: + version "0.17.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" + integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "1.8.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.2: + version "1.14.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" + integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= + dependencies: + memory-pager "^1.0.2" + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +ws@^8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b" + integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA== diff --git a/bot/Dockerfile b/bot/Dockerfile new file mode 100644 index 0000000..e29635c --- /dev/null +++ b/bot/Dockerfile @@ -0,0 +1,13 @@ +FROM node:16 as build +WORKDIR /build/ +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile +COPY . . +RUN yarn build + +FROM node:16 as prod +WORKDIR /app/ +COPY --from=build /build/package.json /build/yarn.lock ./ +COPY --from=build /build/dist ./dist +RUN yarn install --production --frozen-lockfile +CMD ["yarn", "start"] diff --git a/README.md b/bot/README.md similarity index 100% rename from README.md rename to bot/README.md diff --git a/package.json b/bot/package.json similarity index 93% rename from package.json rename to bot/package.json index c1a6bb7..85f4188 100644 --- a/package.json +++ b/bot/package.json @@ -13,6 +13,7 @@ "author": "", "license": "ISC", "dependencies": { + "@janderedev/revolt.js": "^5.2.8-patch.1", "@types/monk": "^6.0.0", "axios": "^0.22.0", "dayjs": "^1.10.7", @@ -21,7 +22,6 @@ "form-data": "^4.0.0", "log75": "^2.2.0", "monk": "^7.3.4", - "revolt.js": "^5.2.3", "ulid": "^2.3.0", "xlsx": "^0.17.3" }, diff --git a/src/bot/commands/ban.ts b/bot/src/bot/commands/ban.ts similarity index 100% rename from src/bot/commands/ban.ts rename to bot/src/bot/commands/ban.ts diff --git a/src/bot/commands/bot_managers.ts b/bot/src/bot/commands/bot_managers.ts similarity index 97% rename from src/bot/commands/bot_managers.ts rename to bot/src/bot/commands/bot_managers.ts index dc5f39e..c1d4e05 100644 --- a/src/bot/commands/bot_managers.ts +++ b/bot/src/bot/commands/bot_managers.ts @@ -2,7 +2,7 @@ import Command from "../../struct/Command"; import { hasPerm, parseUser } from "../util"; import ServerConfig from "../../struct/ServerConfig"; import { client } from "../.."; -import { User } from "revolt.js/dist/maps/Users"; +import { User } from "@janderedev/revolt.js/dist/maps/Users"; import MessageCommandContext from "../../struct/MessageCommandContext"; const SYNTAX = '/admin add @user; /admin remove @user; /admin list'; diff --git a/src/bot/commands/botadm.ts b/bot/src/bot/commands/botadm.ts similarity index 98% rename from src/bot/commands/botadm.ts rename to bot/src/bot/commands/botadm.ts index 8fa1ac0..416d287 100644 --- a/src/bot/commands/botadm.ts +++ b/bot/src/bot/commands/botadm.ts @@ -6,7 +6,7 @@ import child_process from 'child_process'; import fs from 'fs'; import path from 'path'; import { wordlist } from "../modules/user_scan"; -import { User } from "revolt.js/dist/maps/Users"; +import { User } from "@janderedev/revolt.js/dist/maps/Users"; import { adminBotLog } from "../logging"; // id: expireDate @@ -37,7 +37,7 @@ export default { description: 'Bot administration', removeEmptyArgs: true, restrict: 'BOTOWNER', - category: 'moderation', + category: 'owner', run: async (message: MessageCommandContext, args: string[]) => { if (!args.length) return message.reply('No subcommand specified. Available subcommands: ' + SUBCOMMANDS.join(', ')); diff --git a/src/bot/commands/botctl.ts b/bot/src/bot/commands/botctl.ts similarity index 100% rename from src/bot/commands/botctl.ts rename to bot/src/bot/commands/botctl.ts diff --git a/src/bot/commands/debug.ts b/bot/src/bot/commands/debug.ts similarity index 100% rename from src/bot/commands/debug.ts rename to bot/src/bot/commands/debug.ts diff --git a/src/bot/commands/eval.ts b/bot/src/bot/commands/eval.ts similarity index 96% rename from src/bot/commands/eval.ts rename to bot/src/bot/commands/eval.ts index b5ec43d..4eba7b3 100644 --- a/src/bot/commands/eval.ts +++ b/bot/src/bot/commands/eval.ts @@ -1,5 +1,5 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { inspect } from 'util'; import { client } from "../.."; import MessageCommandContext from "../../struct/MessageCommandContext"; diff --git a/src/bot/commands/help.ts b/bot/src/bot/commands/help.ts similarity index 98% rename from src/bot/commands/help.ts rename to bot/src/bot/commands/help.ts index cc65701..68defc1 100644 --- a/src/bot/commands/help.ts +++ b/bot/src/bot/commands/help.ts @@ -1,5 +1,5 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { commands, DEFAULT_PREFIX, ownerIDs } from "../modules/command_handler"; import CommandCategory from "../../struct/CommandCategory"; import MessageCommandContext from "../../struct/MessageCommandContext"; diff --git a/src/bot/commands/kick.ts b/bot/src/bot/commands/kick.ts similarity index 97% rename from src/bot/commands/kick.ts rename to bot/src/bot/commands/kick.ts index 2cf43ab..a15ceac 100644 --- a/src/bot/commands/kick.ts +++ b/bot/src/bot/commands/kick.ts @@ -1,4 +1,4 @@ -import { Member } from "revolt.js/dist/maps/Members"; +import { Member } from "@janderedev/revolt.js/dist/maps/Members"; import { ulid } from "ulid"; import { client } from "../.."; import Infraction from "../../struct/antispam/Infraction"; diff --git a/bot/src/bot/commands/login.ts b/bot/src/bot/commands/login.ts new file mode 100644 index 0000000..f49094a --- /dev/null +++ b/bot/src/bot/commands/login.ts @@ -0,0 +1,62 @@ +import { FindOneResult } from "monk"; +import { client } from "../.."; +import Command from "../../struct/Command"; +import MessageCommandContext from "../../struct/MessageCommandContext"; +import PendingLogin from "../../struct/PendingLogin"; +import logger from "../logger"; +import { DEFAULT_PREFIX } from "../modules/command_handler"; + +export default { + name: 'login', + aliases: null, + description: 'Log into the web dashboard', + category: 'misc', + run: async (message: MessageCommandContext, args: string[]) => { + try { + const code = args.shift(); + if (!code) { + return message.reply(`If you're trying to log in, you can access the dashboard ` + + `[here](${process.env.WEB_UI_URL || 'https://automod.janderedev.xyz'}).\n\n` + + `If you already have a code, you can use \`${DEFAULT_PREFIX}login [Code]\`.`); + } + + const login: FindOneResult = await client.db.get('pending_logins').findOne({ + code, + user: message.author_id, + confirmed: false, + exchanged: false, + invalid: false, + expires: { + $gt: Date.now(), + }, + }); + + if (!login) return message.reply(`Unknown code. Make sure you're logged into the correct account.`); + + if (login.requirePhishingConfirmation) { + logger.info(`Showing phishing warning to ${message.author_id}`); + await Promise.all([ + message.reply( + `# If someone told you to run this, stop!\n` + + `This could give an attacker access to all servers you're using AutoMod in.\n` + + `If someone else told you to run this command, **block them and ignore this.**\n\n` + + `Otherwise, if this was you trying to log in from <${process.env.WEB_UI_URL || 'https://automod.janderedev.xyz'}>, \n` + + `you can run this command again to continue.\n` + + `##### You're seeing this because this is the first time you're trying to log in. Stay safe!` + ), + client.db.get('pending_logins').update({ _id: login._id }, { $set: { requirePhishingConfirmation: false } }), + ]); + return; + } + + await Promise.all([ + message.reply(`Successfully logged in.\n\n` + + `If this wasn't you, run \`${DEFAULT_PREFIX}logout ${code}\` immediately.`), + client.db.get('pending_logins').update({ _id: login._id }, { $set: { confirmed: true } }), + ]); + } catch(e) { + console.error(e); + message.reply(`An error occurred: ${e}`); + } + } +} as Command; diff --git a/bot/src/bot/commands/logout.ts b/bot/src/bot/commands/logout.ts new file mode 100644 index 0000000..29dcb10 --- /dev/null +++ b/bot/src/bot/commands/logout.ts @@ -0,0 +1,64 @@ +import { FindOneResult, FindResult } from "monk"; +import { client } from "../.."; +import Command from "../../struct/Command"; +import MessageCommandContext from "../../struct/MessageCommandContext"; +import PendingLogin from "../../struct/PendingLogin"; +import { DEFAULT_PREFIX } from "../modules/command_handler"; + +export default { + name: 'logout', + aliases: null, + description: 'Log out of sessions created with /login', + category: 'misc', + run: async (message: MessageCommandContext, args: string[]) => { + try { + const code = args.shift(); + if (!code) { + return message.reply(`### No code provided.\n` + + `You can invalidate a session by using \`${DEFAULT_PREFIX}logout [Code]\`, ` + + `or log out everywhere with \`${DEFAULT_PREFIX}logout ALL\``); + } + + if (code.toLowerCase() == 'all') { + const [ attempts, sessions ]: FindResult[] = await Promise.all([ + client.db.get('pending_logins').find({ user: message.author_id }), + client.db.get('sessions').find({ user: message.author_id }), + ]); + + if (attempts.length == 0 && sessions.length == 0) return message.reply('There are no sessions to invalidate.'); + + await Promise.all([ + client.db.get('pending_logins').update({ _id: { $in: attempts.map(a => a._id) } }, { $set: { invalid: true } }), + client.db.get('sessions').update({ _id: { $in: sessions.map(a => a._id) } }, { $set: { invalid: true } }), + ]); + + message.reply(`Successfully invalidated ${attempts.length} codes and ${sessions.length} sessions.`); + } else { + const loginAttempt: FindOneResult = await client.db.get('pending_logins') + .findOne({ + code: code.toUpperCase(), + user: message.author_id, + }); + + if (!loginAttempt || loginAttempt.invalid) { + return message.reply('That code doesn\'t seem to exist.'); + } + + await client.db.get('pending_logins').update({ _id: loginAttempt._id }, { $set: { invalid: true } }); + + if (loginAttempt.exchanged) { + const session: FindOneResult = await client.db.get('sessions').findOne({ nonce: loginAttempt.nonce }); + if (session) { + await client.db.get('sessions').update({ _id: session._id }, { $set: { invalid: true } }); + return message.reply(`Successfully invalidated code and terminated associated session.`); + } + } + + message.reply(`Successfully invalidated code.`); + } + } catch(e) { + console.error(e); + message.reply(`An error occurred: ${e}`); + } + } +} as Command; diff --git a/src/bot/commands/moderator.ts b/bot/src/bot/commands/moderator.ts similarity index 95% rename from src/bot/commands/moderator.ts rename to bot/src/bot/commands/moderator.ts index 9279768..b7158ce 100644 --- a/src/bot/commands/moderator.ts +++ b/bot/src/bot/commands/moderator.ts @@ -1,9 +1,9 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { isBotManager, NO_MANAGER_MSG, parseUser } from "../util"; import ServerConfig from "../../struct/ServerConfig"; import { client } from "../.."; -import { User } from "revolt.js/dist/maps/Users"; +import { User } from "@janderedev/revolt.js/dist/maps/Users"; import MessageCommandContext from "../../struct/MessageCommandContext"; const SYNTAX = '/mod add @user; /mod remove @user; /mod list'; diff --git a/src/bot/commands/ping.ts b/bot/src/bot/commands/ping.ts similarity index 91% rename from src/bot/commands/ping.ts rename to bot/src/bot/commands/ping.ts index 426ba4b..5f3031a 100644 --- a/src/bot/commands/ping.ts +++ b/bot/src/bot/commands/ping.ts @@ -1,5 +1,5 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { client } from "../.."; import MessageCommandContext from "../../struct/MessageCommandContext"; diff --git a/src/bot/commands/prefix.ts b/bot/src/bot/commands/prefix.ts similarity index 97% rename from src/bot/commands/prefix.ts rename to bot/src/bot/commands/prefix.ts index fd6539f..d45d736 100644 --- a/src/bot/commands/prefix.ts +++ b/bot/src/bot/commands/prefix.ts @@ -1,5 +1,5 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { client } from "../.."; import ServerConfig from "../../struct/ServerConfig"; import { DEFAULT_PREFIX } from "../modules/command_handler"; diff --git a/src/bot/commands/purge.ts b/bot/src/bot/commands/purge.ts similarity index 98% rename from src/bot/commands/purge.ts rename to bot/src/bot/commands/purge.ts index 1eb09c0..97181cc 100644 --- a/src/bot/commands/purge.ts +++ b/bot/src/bot/commands/purge.ts @@ -1,5 +1,5 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { decodeTime } from 'ulid'; import { isModerator, parseUser } from "../util"; import MessageCommandContext from "../../struct/MessageCommandContext"; diff --git a/src/bot/commands/settings.ts b/bot/src/bot/commands/settings.ts similarity index 95% rename from src/bot/commands/settings.ts rename to bot/src/bot/commands/settings.ts index c9963cf..301ae5d 100644 --- a/src/bot/commands/settings.ts +++ b/bot/src/bot/commands/settings.ts @@ -1,5 +1,5 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { client } from "../.."; import AutomodSettings from "../../struct/antispam/AutomodSettings"; import AntispamRule from "../../struct/antispam/AntispamRule"; diff --git a/src/bot/commands/shell_eval.ts b/bot/src/bot/commands/shell_eval.ts similarity index 95% rename from src/bot/commands/shell_eval.ts rename to bot/src/bot/commands/shell_eval.ts index 466309b..d46b3d9 100644 --- a/src/bot/commands/shell_eval.ts +++ b/bot/src/bot/commands/shell_eval.ts @@ -1,5 +1,5 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { exec } from 'child_process'; import MessageCommandContext from "../../struct/MessageCommandContext"; diff --git a/src/bot/commands/test.ts b/bot/src/bot/commands/test.ts similarity index 71% rename from src/bot/commands/test.ts rename to bot/src/bot/commands/test.ts index 03077e6..e62788f 100644 --- a/src/bot/commands/test.ts +++ b/bot/src/bot/commands/test.ts @@ -1,5 +1,5 @@ import Command from "../../struct/Command"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import MessageCommandContext from "../../struct/MessageCommandContext"; export default { @@ -8,6 +8,6 @@ export default { description: 'Test command', category: 'misc', run: (message: MessageCommandContext, args: string[]) => { - message.reply('Beep boop.'); + setTimeout(() => message.reply('Beep boop.'), 1000); } } as Command; diff --git a/src/bot/commands/unban.ts b/bot/src/bot/commands/unban.ts similarity index 100% rename from src/bot/commands/unban.ts rename to bot/src/bot/commands/unban.ts diff --git a/src/bot/commands/warn.ts b/bot/src/bot/commands/warn.ts similarity index 100% rename from src/bot/commands/warn.ts rename to bot/src/bot/commands/warn.ts diff --git a/src/bot/commands/warns.ts b/bot/src/bot/commands/warns.ts similarity index 100% rename from src/bot/commands/warns.ts rename to bot/src/bot/commands/warns.ts diff --git a/src/bot/commands/whitelist.ts b/bot/src/bot/commands/whitelist.ts similarity index 97% rename from src/bot/commands/whitelist.ts rename to bot/src/bot/commands/whitelist.ts index 9f224cc..0230632 100644 --- a/src/bot/commands/whitelist.ts +++ b/bot/src/bot/commands/whitelist.ts @@ -1,5 +1,5 @@ -import { Message } from "revolt.js/dist/maps/Messages"; -import { User } from "revolt.js/dist/maps/Users"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; +import { User } from "@janderedev/revolt.js/dist/maps/Users"; import { client } from "../.."; import Command from "../../struct/Command"; import MessageCommandContext from "../../struct/MessageCommandContext"; diff --git a/src/bot/db.ts b/bot/src/bot/db.ts similarity index 100% rename from src/bot/db.ts rename to bot/src/bot/db.ts diff --git a/src/bot/logger.ts b/bot/src/bot/logger.ts similarity index 100% rename from src/bot/logger.ts rename to bot/src/bot/logger.ts diff --git a/src/bot/logging.ts b/bot/src/bot/logging.ts similarity index 100% rename from src/bot/logging.ts rename to bot/src/bot/logging.ts diff --git a/src/bot/modules/antispam.ts b/bot/src/bot/modules/antispam.ts similarity index 98% rename from src/bot/modules/antispam.ts rename to bot/src/bot/modules/antispam.ts index d0cda7a..7ddb375 100644 --- a/src/bot/modules/antispam.ts +++ b/bot/src/bot/modules/antispam.ts @@ -1,4 +1,4 @@ -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { ulid } from "ulid"; import { client } from "../.."; import AntispamRule from "../../struct/antispam/AntispamRule"; diff --git a/bot/src/bot/modules/api/server_details.ts b/bot/src/bot/modules/api/server_details.ts new file mode 100644 index 0000000..a5bb3d2 --- /dev/null +++ b/bot/src/bot/modules/api/server_details.ts @@ -0,0 +1,92 @@ +import { Member } from "@janderedev/revolt.js/dist/maps/Members"; +import { User } from "@janderedev/revolt.js/dist/maps/Users"; +import { client } from "../../.."; +import ServerConfig from "../../../struct/ServerConfig"; +import { getPermissionLevel } from "../../util"; +import { wsEvents, WSResponse } from "../api_communication"; + +type ReqData = { user: string, server: string } +type APIUser = { id: string, username?: string, avatarURL?: string } +type APIChannel = { id: string, name: string, icon?: string, type: 'VOICE'|'TEXT', nsfw: boolean } + +type ServerDetails = { + id: string, + perms: 0|1|2|3, + name: string, + description?: string, + iconURL?: string, + bannerURL?: string, + serverConfig?: ServerConfig, + users: APIUser[], + channels: APIChannel[], +} + +wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSResponse) => void) => { + try { + const server = client.servers.get(data.server); + if (!server) return cb({ success: false, error: 'The requested server could not be found', statusCode: 404 }); + + let user: User; + try { + user = client.users.get(data.user) || await client.users.fetch(data.user); + } catch(e) { + cb({ success: false, error: 'The requested user could not be found', statusCode: 404 }); + return; + } + + let member: Member; + try { + member = await server.fetchMember(user); + } catch(e) { + cb({ success: false, error: 'The requested user is not a member of that server', statusCode: 401 }); + return; + } + + const serverConfig: ServerConfig = await client.db.get('servers').findOne({ id: server._id }); + + // todo: remove unwanted keys from server config + + async function fetchUser(id: string) { + try { + return client.users.get(id) || await client.users.fetch(id); + } catch(e) { + throw id; // this is stupid but idc + } + } + + const users = await Promise.allSettled([ + ...(serverConfig.botManagers?.map(u => fetchUser(u)) ?? []), + ...(serverConfig.moderators?.map(u => fetchUser(u)) ?? []), + fetchUser(user._id), + ]); + + const response: ServerDetails = { + id: server._id, + name: server.name, + perms: await getPermissionLevel(member, server), + description: server.description ?? undefined, + bannerURL: server.generateBannerURL(), + iconURL: server.generateIconURL(), + serverConfig, + users: users.map( + u => u.status == 'fulfilled' + ? { id: u.value._id, avatarURL: u.value.generateAvatarURL(), username: u.value.username } + : { id: u.reason } + ), + channels: server.channels.filter(c => c != undefined).map(c => ({ + id: c!._id, + name: c!.name ?? '', + nsfw: c!.nsfw ?? false, + type: c!.channel_type == 'VoiceChannel' ? 'VOICE' : 'TEXT', + icon: c!.generateIconURL(), + })), + } + + cb({ success: true, server: response }); + } catch(e) { + console.error(e); + cb({ success: false, error: `${e}` }); + } +}); + +export { APIUser } diff --git a/bot/src/bot/modules/api/servers.ts b/bot/src/bot/modules/api/servers.ts new file mode 100644 index 0000000..6aab4d6 --- /dev/null +++ b/bot/src/bot/modules/api/servers.ts @@ -0,0 +1,54 @@ +import { User } from '@janderedev/revolt.js/dist/maps/Users'; +import { client } from '../../..'; +import { getPermissionLevel, isBotManager } from '../../util'; +import { wsEvents, WSResponse } from '../api_communication'; + +type ReqData = { user: string } + +wsEvents.on('req:getUserServers', async (data: ReqData, cb: (data: WSResponse) => void) => { + try { + let user: User; + try { + user = client.users.get(data.user) || await client.users.fetch(data.user); + } catch(e) { + cb({ success: false, error: 'The requested user could not be found', statusCode: 404 }); + return; + } + + const mutuals = await user.fetchMutual(); + + type ServerResponse = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string } + + const promises: Promise[] = []; + + for (const sid of mutuals.servers) { + promises.push(new Promise(async (resolve, reject) => { + try { + const server = client.servers.get(sid); + if (!server) return reject('Server not found'); + const perms = await getPermissionLevel(user, server); + resolve({ + id: sid, + perms, + name: server.name, + bannerURL: server.generateBannerURL(), + iconURL: server.generateIconURL({}), + }); + } catch(e) { + console.error(e); + reject(`${e}`); + } + })); + } + + cb({ + success: true, + servers: (await Promise.allSettled(promises)).map( + p => p.status == 'fulfilled' ? p.value : undefined + ), + }); + } catch(e) { + console.error(e); + cb({ success: false, error: `${e}` }); + } +}); diff --git a/bot/src/bot/modules/api/users.ts b/bot/src/bot/modules/api/users.ts new file mode 100644 index 0000000..0a4ced6 --- /dev/null +++ b/bot/src/bot/modules/api/users.ts @@ -0,0 +1,38 @@ +import { User } from "@janderedev/revolt.js/dist/maps/Users"; +import { client } from "../../.."; +import { getPermissionLevel, parseUser } from "../../util"; +import { wsEvents, WSResponse } from "../api_communication"; +import { APIUser } from "./server_details"; + +wsEvents.on('req:getPermissionLevel', async (data: { user: string, server: string }, cb: (data: WSResponse) => void) => { + try { + const server = client.servers.get(data.server); + if (!server) return cb({ success: false, error: 'The requested server could not be found', statusCode: 404 }); + + let user: User; + try { + user = client.users.get(data.user) || await client.users.fetch(data.user); + } catch(e) { + cb({ success: false, error: 'The requested user could not be found', statusCode: 404 }); + return; + } + + return cb({ success: true, level: await getPermissionLevel(user, server) }) + } catch(e) { + console.error(e); + cb({ success: false, error: `${e}` }); + } +}); + +wsEvents.on('req:getUser', async (data: { user: string }, cb: (data: WSResponse) => void) => { + try { + const user = await parseUser(data.user); + if (!user) + cb({ success: false, statusCode: 404, error: 'User could not be found' }); + else + cb({ success: true, user: { id: user._id, username: user.username, avatarURL: user.generateAvatarURL() } as APIUser }); + } catch(e) { + console.error(e); + cb({ success: false, error: `${e}` }); + } +}); diff --git a/bot/src/bot/modules/api_communication.ts b/bot/src/bot/modules/api_communication.ts new file mode 100644 index 0000000..c9fcc9c --- /dev/null +++ b/bot/src/bot/modules/api_communication.ts @@ -0,0 +1,143 @@ +/** + * This handles communication with the API server. + */ + +import ws from "ws"; +import logger from "../logger"; +import crypto from 'crypto'; +import { client as bot } from '../..'; +import { EventEmitter } from "events"; +import { parseUser } from "../util"; +import PendingLogin from "../../struct/PendingLogin"; +import { ulid } from "ulid"; + +const wsEvents = new EventEmitter(); +const { API_WS_URL, API_WS_TOKEN } = process.env; +const wsQueue: { [key: string]: string }[] = []; +let client: ws|undefined = undefined; + +type WSResponse = { success: false, error: string, statusCode?: number } | { success: true, [key: string]: any } + +if (!API_WS_URL || !API_WS_TOKEN) + logger.info("$API_WS_URL or $API_WS_TOKEN not found."); +else { + logger.info(`$API_WS_URL and $API_WS_TOKEN set; Connecting to ${API_WS_URL}`); + connect(); +} + +function connect() { + if (client && client.readyState == ws.OPEN) client.close(); + client = new ws(API_WS_URL!, { headers: { authorization: API_WS_TOKEN! } }); + + client.once("open", () => { + logger.debug("WS connected"); + if (wsQueue.length > 0) { + logger.debug(`Attempting to send ${wsQueue.length} queued WS messages`); + + while (wsQueue.length > 0) { + if (client?.readyState != ws.OPEN) break; + const data = JSON.stringify(wsQueue.shift()); + logger.debug(`[WS] [FROM QUEUE] [>] ${data}`); + client.send(data); + } + } + }); + + client.once("close", () => { + client = undefined; + logger.warn(`WS closed, reconnecting in 3 seconds`); + setTimeout(connect, 3000); + }); + + client.once('error', (err) => { + client = undefined; + logger.warn(`WS: ${err}`); + }); + + client.on('message', (msg) => { + logger.debug(`[WS] [<] ${msg.toString('utf8')}`); + try { + const jsonMsg = JSON.parse(msg.toString('utf8')); + wsEvents.emit('message', jsonMsg); + if (jsonMsg['nonce'] && jsonMsg['type']) { + const hasListeners = wsEvents.emit(`req:${jsonMsg.type}`, jsonMsg.data, (res: { [key: string]: any }) => { + wsSend({ nonce: jsonMsg.nonce, type: `response:${jsonMsg.nonce}`, data: res }); + }); + + if (!hasListeners) { + wsSend({ + nonce: jsonMsg.nonce, + type: `response:${jsonMsg.nonce}`, + data: { + success: false, + error: 'No event listeners available for event' + } + }); + } + } + } catch(e) { console.error(e) } + }); +} + +function wsSend(data: { [key: string]: any }) { + if (client && client.readyState == client.OPEN) { + logger.debug(`[WS] [>] ${JSON.stringify(data)}`); + client.send(JSON.stringify(data)); + } else { + logger.debug(`[WS] [QUEUED] [>] ${JSON.stringify(data)}`); + wsQueue.push(data); + } +} + +wsEvents.on('req:test', (data: any, res: (data: any) => void) => { + res({ received: data }); +}); + +wsEvents.on('req:requestLogin', async (data: any, cb: (data: WSResponse) => void) => { + try { + const user = await parseUser(data.user); + if (!user) + return cb({ success: false, statusCode: 404, error: `The specified user could not be found` }); + + let code: string|null = null; + while (!code) { + const c = crypto.randomBytes(8).toString('hex'); + const found = await bot.db.get('pending_logins').find({ code: c, user: user._id, confirmed: false }); + if (found.length > 0) continue; + code = c.substring(0, 8).toUpperCase(); + } + + logger.info(`Attempted login for user ${user._id} with code ${code}`); + + const nonce = ulid(); + + const [previousLogins, currentValidLogins] = await Promise.all([ + bot.db.get('pending_logins').find({ user: user._id, confirmed: true }), + bot.db.get('pending_logins').find({ user: user._id, confirmed: false, expires: { $gt: Date.now() } }), + ]); + + if (currentValidLogins.length >= 5) return cb({ success: false, statusCode: 403, error: 'Too many pending logins. Try again later.' }); + + await bot.db.get('pending_logins').insert({ + code, + expires: Date.now() + (1000 * 60 * 15), // Expires in 15 minutes + user: user._id, + nonce: nonce, + confirmed: false, + requirePhishingConfirmation: previousLogins.length == 0, + exchanged: false, + invalid: false, + } as PendingLogin); + + cb({ success: true, uid: user._id, nonce, code }); + } catch(e) { + console.error(e); + cb({ success: false, error: `${e}` }); + } +}); + +export { wsEvents, wsSend, WSResponse } + +import('./api/servers'); +import('./api/server_details'); +import('./api/users'); diff --git a/src/bot/modules/command_handler.ts b/bot/src/bot/modules/command_handler.ts similarity index 98% rename from src/bot/modules/command_handler.ts rename to bot/src/bot/modules/command_handler.ts index 9c4a5f8..3f3d180 100644 --- a/src/bot/modules/command_handler.ts +++ b/bot/src/bot/modules/command_handler.ts @@ -9,7 +9,6 @@ import checkCustomRules from "./custom_rules/custom_rules"; import MessageCommandContext from "../../struct/MessageCommandContext"; import { fileURLToPath } from 'url'; import { getOwnMemberInServer, hasPermForChannel } from "../util"; -import { prepareMessage } from "./prepare_message"; import { isSudo, updateSudoTimeout } from "../commands/botadm"; // thanks a lot esm @@ -96,7 +95,6 @@ let commands: Command[]; let message: MessageCommandContext = msg as MessageCommandContext; message.serverContext = serverCtx; - prepareMessage(message); logger.info(`Command: ${message.author?.username} (${message.author?._id}) in ${message.channel?.server?.name} (${message.channel?.server?._id}): ${message.content}`); diff --git a/src/bot/modules/custom_rules/actions/delete.ts b/bot/src/bot/modules/custom_rules/actions/delete.ts similarity index 79% rename from src/bot/modules/custom_rules/actions/delete.ts rename to bot/src/bot/modules/custom_rules/actions/delete.ts index b721089..ea065aa 100644 --- a/src/bot/modules/custom_rules/actions/delete.ts +++ b/bot/src/bot/modules/custom_rules/actions/delete.ts @@ -1,4 +1,4 @@ -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import CustomRuleAction from "../../../../struct/antispam/CustomRuleAction"; async function execute(message: Message, action: CustomRuleAction) { diff --git a/src/bot/modules/custom_rules/actions/sendMessage.ts b/bot/src/bot/modules/custom_rules/actions/sendMessage.ts similarity index 90% rename from src/bot/modules/custom_rules/actions/sendMessage.ts rename to bot/src/bot/modules/custom_rules/actions/sendMessage.ts index 48962ec..b1172cf 100644 --- a/src/bot/modules/custom_rules/actions/sendMessage.ts +++ b/bot/src/bot/modules/custom_rules/actions/sendMessage.ts @@ -1,4 +1,4 @@ -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { client } from "../../../.."; import CustomRuleAction from "../../../../struct/antispam/CustomRuleAction"; diff --git a/src/bot/modules/custom_rules/actions/warn.ts b/bot/src/bot/modules/custom_rules/actions/warn.ts similarity index 94% rename from src/bot/modules/custom_rules/actions/warn.ts rename to bot/src/bot/modules/custom_rules/actions/warn.ts index 805d531..56fa028 100644 --- a/src/bot/modules/custom_rules/actions/warn.ts +++ b/bot/src/bot/modules/custom_rules/actions/warn.ts @@ -1,4 +1,4 @@ -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import CustomRuleAction from "../../../../struct/antispam/CustomRuleAction"; import { storeInfraction } from '../../../util'; import Infraction from "../../../../struct/antispam/Infraction"; diff --git a/src/bot/modules/custom_rules/custom_rules.ts b/bot/src/bot/modules/custom_rules/custom_rules.ts similarity index 97% rename from src/bot/modules/custom_rules/custom_rules.ts rename to bot/src/bot/modules/custom_rules/custom_rules.ts index b39aaad..e5e18aa 100644 --- a/src/bot/modules/custom_rules/custom_rules.ts +++ b/bot/src/bot/modules/custom_rules/custom_rules.ts @@ -1,4 +1,4 @@ -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { client } from "../../.."; import ServerConfig from "../../../struct/ServerConfig"; import logger from "../../logger"; diff --git a/src/bot/modules/custom_rules/message_content_trigger.ts b/bot/src/bot/modules/custom_rules/message_content_trigger.ts similarity index 98% rename from src/bot/modules/custom_rules/message_content_trigger.ts rename to bot/src/bot/modules/custom_rules/message_content_trigger.ts index 4321b39..5bf5c9d 100644 --- a/src/bot/modules/custom_rules/message_content_trigger.ts +++ b/bot/src/bot/modules/custom_rules/message_content_trigger.ts @@ -1,4 +1,4 @@ -import { Message } from "revolt.js/dist/maps/Messages"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { client } from "../../.."; import CustomRuleTrigger from "../../../struct/antispam/CustomRuleTrigger"; import VM from 'vm'; diff --git a/src/bot/modules/event_handler.ts b/bot/src/bot/modules/event_handler.ts similarity index 100% rename from src/bot/modules/event_handler.ts rename to bot/src/bot/modules/event_handler.ts diff --git a/src/bot/modules/mod_logs.ts b/bot/src/bot/modules/mod_logs.ts similarity index 98% rename from src/bot/modules/mod_logs.ts rename to bot/src/bot/modules/mod_logs.ts index 5ba5a8f..6d36bef 100644 --- a/src/bot/modules/mod_logs.ts +++ b/bot/src/bot/modules/mod_logs.ts @@ -1,5 +1,5 @@ -import { Member } from "revolt.js/dist/maps/Members"; -import { Server } from "revolt.js/dist/maps/Servers"; +import { Member } from "@janderedev/revolt.js/dist/maps/Members"; +import { Server } from "@janderedev/revolt.js/dist/maps/Servers"; import { client } from "../.."; import Infraction from "../../struct/antispam/Infraction"; import LogMessage from "../../struct/LogMessage"; diff --git a/src/bot/modules/tempbans.ts b/bot/src/bot/modules/tempbans.ts similarity index 100% rename from src/bot/modules/tempbans.ts rename to bot/src/bot/modules/tempbans.ts diff --git a/src/bot/modules/user_scan.ts b/bot/src/bot/modules/user_scan.ts similarity index 99% rename from src/bot/modules/user_scan.ts rename to bot/src/bot/modules/user_scan.ts index 440eeb6..64475ec 100644 --- a/src/bot/modules/user_scan.ts +++ b/bot/src/bot/modules/user_scan.ts @@ -2,7 +2,7 @@ import { client } from "../.."; import fs from 'fs'; import { FindOneResult } from "monk"; import ScannedUser from "../../struct/ScannedUser"; -import { Member } from "revolt.js/dist/maps/Members"; +import { Member } from "@janderedev/revolt.js/dist/maps/Members"; import ServerConfig from "../../struct/ServerConfig"; import logger from "../logger"; import { sendLogMessage } from "../util"; diff --git a/src/bot/util.ts b/bot/src/bot/util.ts similarity index 91% rename from src/bot/util.ts rename to bot/src/bot/util.ts index c35ea5e..24691f5 100644 --- a/src/bot/util.ts +++ b/bot/src/bot/util.ts @@ -1,19 +1,19 @@ -import { Member } from "revolt.js/dist/maps/Members"; -import { User } from "revolt.js/dist/maps/Users"; +import { Member } from "@janderedev/revolt.js/dist/maps/Members"; +import { User } from "@janderedev/revolt.js/dist/maps/Users"; import { client } from ".."; import Infraction from "../struct/antispam/Infraction"; import ServerConfig from "../struct/ServerConfig"; import FormData from 'form-data'; import axios from 'axios'; -import { Server } from "revolt.js/dist/maps/Servers"; +import { Server } from "@janderedev/revolt.js/dist/maps/Servers"; import LogConfig from "../struct/LogConfig"; import LogMessage from "../struct/LogMessage"; import { ColorResolvable, MessageEmbed } from "discord.js"; import logger from "./logger"; import { ulid } from "ulid"; -import { Channel } from "revolt.js/dist/maps/Channels"; -import { ChannelPermission, ServerPermission } from "revolt.js"; -import { Message } from "revolt.js/dist/maps/Messages"; +import { Channel } from "@janderedev/revolt.js/dist/maps/Channels"; +import { ChannelPermission, ServerPermission } from "@janderedev/revolt.js"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { isSudo } from "./commands/botadm"; @@ -99,6 +99,22 @@ async function checkSudoPermission(message: Message): Promise { return true; } } +async function getPermissionLevel(user: User|Member, server: Server): Promise<0|1|2|3> { + if (isSudo(user instanceof User ? user : (user.user || await client.users.fetch(user._id.user)))) return 2; + + const member = user instanceof User ? await server.fetchMember(user) : user; + if (user instanceof Member) user = user.user!; + + if (hasPerm(member, 'ManageServer')) return 3; + if (hasPerm(member, 'KickMembers')) return 1; + + const config = (await client.db.get('servers').findOne({ id: server._id }) || {}) as ServerConfig; + + if (config.botManagers?.includes(user._id)) return 2; + if (config.moderators?.includes(user._id)) return 1; + + return 0; +} function hasPerm(member: Member, perm: keyof typeof ServerPermission): boolean { let p = ServerPermission[perm]; @@ -292,6 +308,7 @@ export { getOwnMemberInServer, isModerator, isBotManager, + getPermissionLevel, parseUser, parseUserOrId, storeInfraction, diff --git a/src/index.ts b/bot/src/index.ts similarity index 78% rename from src/index.ts rename to bot/src/index.ts index 9bb6623..c6fd57a 100644 --- a/src/index.ts +++ b/bot/src/index.ts @@ -8,7 +8,12 @@ import MongoDB from './bot/db'; logger.info('Initializing client'); let db = MongoDB(); -let client = new AutomodClient({ pongTimeout: 10, onPongTimeout: 'RECONNECT' }, db); +let client = new AutomodClient({ + pongTimeout: 10, + onPongTimeout: 'RECONNECT', + fixReplyCrash: true, + messageTimeoutFix: true +}, db); login(client); export { client } @@ -25,4 +30,5 @@ export { client } import('./bot/modules/event_handler'); import('./bot/modules/tempbans'); import('./bot/modules/user_scan'); + import('./bot/modules/api_communication'); })(); diff --git a/src/struct/AutomodClient.ts b/bot/src/struct/AutomodClient.ts similarity index 95% rename from src/struct/AutomodClient.ts rename to bot/src/struct/AutomodClient.ts index ed34e7e..282703b 100644 --- a/src/struct/AutomodClient.ts +++ b/bot/src/struct/AutomodClient.ts @@ -1,4 +1,4 @@ -import * as Revolt from "revolt.js"; +import * as Revolt from "@janderedev/revolt.js"; import { IMonkManager } from 'monk'; import logger from '../bot/logger'; import { adminBotLog } from "../bot/logging"; diff --git a/src/struct/Command.ts b/bot/src/struct/Command.ts similarity index 83% rename from src/struct/Command.ts rename to bot/src/struct/Command.ts index 3a75f0e..f201f9d 100644 --- a/src/struct/Command.ts +++ b/bot/src/struct/Command.ts @@ -1,4 +1,4 @@ -import { ChannelPermission, ServerPermission } from "revolt.js"; +import { ChannelPermission, ServerPermission } from "@janderedev/revolt.js"; class Command { name: string; diff --git a/src/struct/CommandCategory.ts b/bot/src/struct/CommandCategory.ts similarity index 100% rename from src/struct/CommandCategory.ts rename to bot/src/struct/CommandCategory.ts diff --git a/src/struct/LogConfig.ts b/bot/src/struct/LogConfig.ts similarity index 100% rename from src/struct/LogConfig.ts rename to bot/src/struct/LogConfig.ts diff --git a/src/struct/LogMessage.ts b/bot/src/struct/LogMessage.ts similarity index 100% rename from src/struct/LogMessage.ts rename to bot/src/struct/LogMessage.ts diff --git a/src/struct/MessageCommandContext.ts b/bot/src/struct/MessageCommandContext.ts similarity index 65% rename from src/struct/MessageCommandContext.ts rename to bot/src/struct/MessageCommandContext.ts index 557564e..6af8d30 100644 --- a/src/struct/MessageCommandContext.ts +++ b/bot/src/struct/MessageCommandContext.ts @@ -1,5 +1,5 @@ -import { Message } from "revolt.js/dist/maps/Messages"; -import { Server } from "revolt.js/dist/maps/Servers"; +import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; +import { Server } from "@janderedev/revolt.js/dist/maps/Servers"; import logger from "../bot/logger"; class MessageCommandContext extends Message { diff --git a/bot/src/struct/PendingLogin.ts b/bot/src/struct/PendingLogin.ts new file mode 100644 index 0000000..2c85391 --- /dev/null +++ b/bot/src/struct/PendingLogin.ts @@ -0,0 +1,12 @@ +class PendingLogin { + user: string; + code: string; + expires: number; + nonce: string; + confirmed: boolean; + requirePhishingConfirmation: boolean; + exchanged: boolean; + invalid: boolean; +} + +export default PendingLogin; diff --git a/src/struct/ScannedUser.ts b/bot/src/struct/ScannedUser.ts similarity index 100% rename from src/struct/ScannedUser.ts rename to bot/src/struct/ScannedUser.ts diff --git a/src/struct/ServerConfig.ts b/bot/src/struct/ServerConfig.ts similarity index 100% rename from src/struct/ServerConfig.ts rename to bot/src/struct/ServerConfig.ts diff --git a/src/struct/TempBan.ts b/bot/src/struct/TempBan.ts similarity index 100% rename from src/struct/TempBan.ts rename to bot/src/struct/TempBan.ts diff --git a/src/struct/antispam/AntispamRule.ts b/bot/src/struct/antispam/AntispamRule.ts similarity index 100% rename from src/struct/antispam/AntispamRule.ts rename to bot/src/struct/antispam/AntispamRule.ts diff --git a/src/struct/antispam/AutomodSettings.ts b/bot/src/struct/antispam/AutomodSettings.ts similarity index 100% rename from src/struct/antispam/AutomodSettings.ts rename to bot/src/struct/antispam/AutomodSettings.ts diff --git a/src/struct/antispam/CustomRule.ts b/bot/src/struct/antispam/CustomRule.ts similarity index 100% rename from src/struct/antispam/CustomRule.ts rename to bot/src/struct/antispam/CustomRule.ts diff --git a/src/struct/antispam/CustomRuleAction.ts b/bot/src/struct/antispam/CustomRuleAction.ts similarity index 100% rename from src/struct/antispam/CustomRuleAction.ts rename to bot/src/struct/antispam/CustomRuleAction.ts diff --git a/src/struct/antispam/CustomRuleTrigger.ts b/bot/src/struct/antispam/CustomRuleTrigger.ts similarity index 100% rename from src/struct/antispam/CustomRuleTrigger.ts rename to bot/src/struct/antispam/CustomRuleTrigger.ts diff --git a/src/struct/antispam/Infraction.ts b/bot/src/struct/antispam/Infraction.ts similarity index 100% rename from src/struct/antispam/Infraction.ts rename to bot/src/struct/antispam/Infraction.ts diff --git a/src/struct/antispam/InfractionType.ts b/bot/src/struct/antispam/InfractionType.ts similarity index 100% rename from src/struct/antispam/InfractionType.ts rename to bot/src/struct/antispam/InfractionType.ts diff --git a/src/struct/antispam/ModerationAction.ts b/bot/src/struct/antispam/ModerationAction.ts similarity index 100% rename from src/struct/antispam/ModerationAction.ts rename to bot/src/struct/antispam/ModerationAction.ts diff --git a/bot/tsconfig.json b/bot/tsconfig.json new file mode 100644 index 0000000..95d3408 --- /dev/null +++ b/bot/tsconfig.json @@ -0,0 +1,100 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Projects */ + // "incremental": true, /* Enable incremental compilation */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ + // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + + /* Modules */ + "module": "ES2020", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "resolveJsonModule": true, /* Enable importing .json files */ + // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ + // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ + "strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ + // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/yarn.lock b/bot/yarn.lock similarity index 96% rename from yarn.lock rename to bot/yarn.lock index a4d7a7f..36275ac 100644 --- a/yarn.lock +++ b/bot/yarn.lock @@ -37,6 +37,23 @@ resolved "https://registry.yarnpkg.com/@insertish/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#5bcd6f73b93efa9ccdb6abf887ae808d40827169" integrity sha512-kFD/p8T4Hkqr992QrdkbW/cQ/W/q2d9MPCobwzBv2PwTKLkCD9RaYDy6m17qRnSLQQ5PU0kHCG8kaOwAqzj1vQ== +"@janderedev/revolt.js@^5.2.8-patch.1": + version "5.2.8-patch.1" + resolved "https://registry.yarnpkg.com/@janderedev/revolt.js/-/revolt.js-5.2.8-patch.1.tgz#e8570090612cb9e0f399f8bc75feed3cbbdfcd2a" + integrity sha512-rUjpp+Nk7/aPdFrSNBorSyvJwIb4fkwRzLB2OODWLesYvlxddJG2PtFujWU4dk3fnQMxpMaQVLq95T2GtsOkdg== + dependencies: + "@insertish/exponential-backoff" "3.1.0-patch.0" + "@insertish/isomorphic-ws" "^4.0.1" + axios "^0.21.4" + eventemitter3 "^4.0.7" + lodash.defaultsdeep "^4.6.1" + lodash.flatten "^4.4.0" + lodash.isequal "^4.5.0" + mobx "^6.3.2" + revolt-api "0.5.3-alpha.12" + ulid "^2.3.0" + ws "^8.2.2" + "@sapphire/async-queue@^1.1.8": version "1.1.9" resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.1.9.tgz#ce69611c8753c4affd905a7ef43061c7eb95c01b" @@ -362,9 +379,9 @@ mime-types@^2.1.12: mime-db "1.50.0" mobx@^6.3.2: - version "6.3.10" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.10.tgz#c3bc715c8f03717b9a2329f9697d42b7998d42e0" - integrity sha512-lfuIN5TGXBNy/5s3ggr1L+IbD+LvfZVlj5q1ZuqyV9AfMtunYQvE8G0WfewS9tgIR3I1q8HJEEbcAOsxEgLwRw== + version "6.3.13" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.13.tgz#93e56a57ee72369f850cf3d6398fd36ee8ef062e" + integrity sha512-zDDKDhYUk9QCHQUdLG+wb4Jv/nXutSLt/P8kkwHyjdbrJO4OZS6QTEsrOnrKM39puqXSrJZHdB6+yRys2NBFFA== mongodb@^3.2.3: version "3.7.2" @@ -499,27 +516,10 @@ require-at@^1.0.6: resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a" integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g== -revolt-api@^0.5.3-alpha.9: - version "0.5.3-alpha.11" - resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3-alpha.11.tgz#ed666c403676de0bc47b1fe3ed59c934c5fbd842" - integrity sha512-5obsvdSIaiW3oVcsFpXYG3FXTPUxwR+0gpPhP+89i2agGdeKhcygaRajZ5HK83/Zmr/VcZnJoSuHc+k3ZUTHyA== - -revolt.js@^5.2.3: - version "5.2.3" - resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.2.3.tgz#770e7fc3256d7d153fadf143acd00f4c3377f4ef" - integrity sha512-tFGt1yij56A1y3EDjTf1HoPmwHskQ0xoks58tWNCPdMvyCyrTih9qBKdHNDDOMUWdGF0qn1pWZmdVSxHriAuOA== - dependencies: - "@insertish/exponential-backoff" "3.1.0-patch.0" - "@insertish/isomorphic-ws" "^4.0.1" - axios "^0.21.4" - eventemitter3 "^4.0.7" - lodash.defaultsdeep "^4.6.1" - lodash.flatten "^4.4.0" - lodash.isequal "^4.5.0" - mobx "^6.3.2" - revolt-api "^0.5.3-alpha.9" - ulid "^2.3.0" - ws "^8.2.2" +revolt-api@0.5.3-alpha.12: + version "0.5.3-alpha.12" + resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3-alpha.12.tgz#78f25b567b840c1fd072595526592a422cb01f25" + integrity sha512-MM42oI5+5JJMnAs3JiOwSQOy/SUYzYs3M8YRC5QI4G6HU7CfyB2HNWh5jFsyRlcLdSi13dGazHm31FUPHsxOzw== safe-buffer@^5.1.1, safe-buffer@^5.1.2: version "5.2.1" @@ -623,9 +623,9 @@ word@~0.3.0: integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== ws@^8.2.2: - version "8.4.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.0.tgz#f05e982a0a88c604080e8581576e2a063802bed6" - integrity sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ== + version "8.4.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b" + integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA== ws@^8.2.3: version "8.3.0" diff --git a/docker-compose.yml.example b/docker-compose.yml.example new file mode 100644 index 0000000..41af434 --- /dev/null +++ b/docker-compose.yml.example @@ -0,0 +1,65 @@ +# Copy this file to `docker-compose.yml` and modify it to your liking. +# Copy `.env.example` to `.env` to configure environment variables. + +version: "3.1" + +services: + bot: + build: ./bot + environment: + - DB_HOST=mongo:27017 + - DB_USERNAME=mogus + - DB_PASS + - DB_NAME=admin + - BOT_TOKEN + - BOT_OWNERS + - LOG_WEBHOOK + - NODE_ENV=production + - API_WS_URL=ws://api:9000/internal/ws + - API_WS_TOKEN=${INTERNAL_API_TOKEN} + - WEB_UI_URL=${PUBLIC_WEB_URL} + - BOT_PREFIX + depends_on: + - mongo + - api + restart: unless-stopped + + api: + build: ./api + environment: + - BOT_API_TOKEN=${INTERNAL_API_TOKEN} + - DB_HOST=mongo:27017 + - DB_USERNAME=mogus + - DB_PASS + - DB_NAME=admin + expose: + - 9000 + ports: + - 0.0.0.0:9000:9000 + restart: unless-stopped + + # If you prefer to host the web app on a different + # platform like Vercel, you can remove this part. + web: + build: + context: ./web + args: + - VITE_API_URL=${PUBLIC_API_URL} + - VITE_BOT_PREFIX=${BOT_PREFIX} + ports: + - 0.0.0.0:8080:80 + restart: unless-stopped + + mongo: + image: mongo + environment: + - MONGO_INITDB_ROOT_USERNAME=mogus + - MONGO_INITDB_ROOT_PASSWORD=${DB_PASS} + expose: + - 27017 + # If you want to access the database from outside + #ports: + # - "0.0.0.0:36602:27017" + volumes: + - ./db:/data/db:rw + restart: unless-stopped diff --git a/src/bot/modules/prepare_message.ts b/src/bot/modules/prepare_message.ts deleted file mode 100644 index 54c5236..0000000 --- a/src/bot/modules/prepare_message.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Message } from "revolt.js/dist/maps/Messages"; -import logger from "../logger"; - -// We modify the way `reply()` works to make sure we -// don't crash if the original message was deleted. - -export function prepareMessage(message: Message) { - message.reply = (...args: Parameters) => { - return new Promise((resolve, reject) => { - message.channel?.sendMessage({ - content: typeof args[0] == 'string' ? args[0] : args[0].content, - replies: [ { id: message._id, mention: args[1] ?? true } ], - }) - ?.then(m => resolve(m)) - .catch(e => { - if (e?.response?.status == 404) { - logger.warn("Replying to message gave 404, trying again without reply"); - if (!message.channel) return reject("Channel does not exist"); - message.channel?.sendMessage(typeof args[0] == 'string' ? { content: args[0] } : args[0]) - .then(resolve) - .catch(reject); - } else reject(e); - }); - }); - } -} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..d451ff1 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..e5ba12c --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,16 @@ +FROM node:16 as build +ARG VITE_API_URL +ARG VITE_BOT_PREFIX +WORKDIR /build/ +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile +COPY . . +RUN yarn build + +FROM node:16 as prod +WORKDIR /app/ +COPY --from=build /build/package.json /build/yarn.lock ./ +COPY --from=build /build/dist ./dist +RUN yarn add --production --frozen-lockfile vite +# Running this with bash -c because it won't exit on ctrl+c otherwise +CMD ["bash", "-c", "yarn preview --port=80 --strictPort=true --clearScreen=false --host"] diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..164b801 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..43ddd1e --- /dev/null +++ b/web/package.json @@ -0,0 +1,32 @@ +{ + "name": "web", + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@mdi/js": "^6.5.95", + "@mdi/react": "^1.5.0", + "@revoltchat/ui": "^1.0.24", + "@types/axios": "^0.14.0", + "@types/core-js": "^2.5.5", + "@types/styled-components": "^5.1.21", + "axios": "^0.25.0", + "core-js": "^3.20.3", + "localforage": "^1.10.0", + "prop-types": "^15.8.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-router-dom": "^6.2.1", + "styled-components": "^5.3.3" + }, + "devDependencies": { + "@types/react": "^17.0.33", + "@types/react-dom": "^17.0.10", + "@vitejs/plugin-react": "^1.0.7", + "typescript": "^4.4.4", + "vite": "^2.7.2" + } +} diff --git a/web/src/App.css b/web/src/App.css new file mode 100644 index 0000000..8da3fde --- /dev/null +++ b/web/src/App.css @@ -0,0 +1,42 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +button { + font-size: calc(10px + 2vmin); +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..2180b24 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,29 @@ +import { Route, BrowserRouter, Routes } from 'react-router-dom'; +import Home from './pages/Home'; +import './App.css'; +import '@revoltchat/ui/src/styles/dark.css'; +import '@revoltchat/ui/src/styles/common.css'; +import RequireAuth from './components/RequireAuth'; +import DashboardHome from './pages/DashboardHome'; +import ServerDashboard from './pages/ServerDashboard'; + +const API_URL = import.meta.env.VITE_API_URL?.toString() + || 'http://localhost:9000'; + +const BOT_PREFIX = import.meta.env.VITE_BOT_PREFIX?.toString() + || '/'; + +function App() { + return ( + + + } /> + } /> + } /> + + + ); +} + +export default App; +export { API_URL, BOT_PREFIX } diff --git a/web/src/assets/channel-default-icon.svg b/web/src/assets/channel-default-icon.svg new file mode 100644 index 0000000..fd6abab --- /dev/null +++ b/web/src/assets/channel-default-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/RequireAuth.tsx b/web/src/components/RequireAuth.tsx new file mode 100644 index 0000000..4cbb037 --- /dev/null +++ b/web/src/components/RequireAuth.tsx @@ -0,0 +1,15 @@ +import { FunctionComponent, useState, useEffect } from "react"; +import Login from "../pages/Login"; +import { getAuth } from "../utils"; + +const RequireAuth: FunctionComponent = (props) => { + const [loggedIn, setLoggedIn] = useState(true); + + useEffect(() => { + getAuth().then(res => setLoggedIn(!!res)); + }); + + return loggedIn ? <>{props.children} : +} + +export default RequireAuth; diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..606a3cf --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import './index.css' +import App from './App' + +ReactDOM.render( + + + , + document.getElementById('root') +) diff --git a/web/src/pages/DashboardHome.tsx b/web/src/pages/DashboardHome.tsx new file mode 100644 index 0000000..3f3d952 --- /dev/null +++ b/web/src/pages/DashboardHome.tsx @@ -0,0 +1,82 @@ +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; +import { FunctionComponent, useCallback, useEffect, useState } from "react"; +import { Button } from '@revoltchat/ui/lib/components/atoms/inputs/Button'; +import { H1 } from '@revoltchat/ui/lib/components/atoms/heading/H1'; +import { H2 } from '@revoltchat/ui/lib/components/atoms/heading/H2'; +import { API_URL } from "../App"; +import { getAuthHeaders } from "../utils"; + +type Server = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string } + +function permissionName(p: number) { + switch(p) { + case 0: return 'User'; + case 1: return 'Moderator'; + case 2: + case 3: return 'Manager'; + default: return 'Unknown'; + } +} + +const Dashboard: FunctionComponent = () => { + const [loading, setLoading] = useState(true); + const [servers, setServers] = useState([] as Server[]); + const navigate = useNavigate(); + + const loadServers = useCallback(async () => { + try { + const res = await axios.get(API_URL + '/dash/servers', { headers: await getAuthHeaders() }); + setServers(res.data.servers); + setLoading(false); + } catch(e) { + console.error(e); + } + }, []); + + useEffect(() => { loadServers() }, []); + + return ( +
+

dashbord

+
+ + { + servers.map(server =>
+ +
+

{server.name} ({permissionName(server.perms)})

+ {server.id} +
+
+ +
+
+
) + } +
+ ); +} + +export default Dashboard; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx new file mode 100644 index 0000000..4113ce4 --- /dev/null +++ b/web/src/pages/Home.tsx @@ -0,0 +1,12 @@ +import { FunctionComponent } from "react"; + +const Home: FunctionComponent = () => { + return ( +
+

todo: web design

+ sign in +
+ ); +} + +export default Home; diff --git a/web/src/pages/Login.tsx b/web/src/pages/Login.tsx new file mode 100644 index 0000000..2828d2d --- /dev/null +++ b/web/src/pages/Login.tsx @@ -0,0 +1,79 @@ +import localforage from "localforage"; +import axios from 'axios'; +import { FunctionComponent, useCallback, useState } from "react"; +import { Button } from '@revoltchat/ui/lib/components/atoms/inputs/Button'; +import { InputBox } from '@revoltchat/ui/lib/components/atoms/inputs/InputBox'; +import { H1 } from '@revoltchat/ui/lib/components/atoms/heading/H1'; +import { H2 } from '@revoltchat/ui/lib/components/atoms/heading/H2'; +import { API_URL, BOT_PREFIX } from "../App"; + +const Login: FunctionComponent = () => { + const [username, setUsername] = useState(''); + const [showInitial, setShowInitial] = useState(true); + const [showSecond, setShowSecond] = useState(false); + const [statusMsg, setStatusMsg] = useState(''); + const [code, setCode] = useState(''); + const [nonce, setNonce] = useState(''); + + const getCode = useCallback(async () => { + if (!username) return; + setShowInitial(false); + + try { + const res = await axios.post(`${API_URL}/login/begin`, { user: username }); + setShowSecond(true); + setCode(res.data.code); + setNonce(res.data.nonce); + setUsername(res.data.uid); + } catch(e: any) { + setStatusMsg(e?.message || e); + setShowInitial(true); + setShowSecond(false); + } + }, [ username ]); + + const getSession = useCallback(async () => { + try { + const res = await axios.post(`${API_URL}/login/complete`, { + nonce, code, user: username + }); + + await localforage.setItem('auth', { user: res.data.user, token: res.data.token }); + + setShowSecond(false); + window.location.reload(); + } catch(e: any) { + setStatusMsg(e?.message || e); + } + }, [ nonce, code, username ]); + + return ( +
+

log in

+ {statusMsg.length ? {statusMsg} :
} + + +
+ ); +} + +export default Login; diff --git a/web/src/pages/ServerDashboard.tsx b/web/src/pages/ServerDashboard.tsx new file mode 100644 index 0000000..e4b9b8c --- /dev/null +++ b/web/src/pages/ServerDashboard.tsx @@ -0,0 +1,569 @@ +import axios from 'axios'; +import React, { FunctionComponent, useCallback, useEffect, useState } from "react"; +import { Button } from '@revoltchat/ui/lib/components/atoms/inputs/Button'; +import { InputBox } from '@revoltchat/ui/lib/components/atoms/inputs/InputBox'; +import { Checkbox } from '@revoltchat/ui/lib/components/atoms/inputs/Checkbox'; +import { ComboBox } from '@revoltchat/ui/lib/components/atoms/inputs/ComboBox'; +import { LineDivider } from '@revoltchat/ui/lib/components/atoms/layout/LineDivider'; +import { H1 } from '@revoltchat/ui/lib/components/atoms/heading/H1'; +import { H3 } from '@revoltchat/ui/lib/components/atoms/heading/H3'; +import { H4 } from '@revoltchat/ui/lib/components/atoms/heading/H4'; +import { H5 } from '@revoltchat/ui/lib/components/atoms/heading/H5'; +import { Icon } from '@mdi/react'; +import { mdiCloseBox } from '@mdi/js'; +import { API_URL } from "../App"; +import { getAuthHeaders } from "../utils"; +import { useParams } from "react-router-dom"; +import defaultChannelIcon from '../assets/channel-default-icon.svg'; + +type User = { id: string, username?: string, avatarURL?: string } +type Channel = { id: string, name: string, icon?: string, type: 'VOICE'|'TEXT', nsfw: boolean } + +type Server = { + id?: string, + perms?: 0|1|2|3, + name?: string, + description?: string, + iconURL?: string, + bannerURL?: string, + serverConfig?: { [key: string]: any }, + users: User[], + channels: Channel[], +} + +type AntispamRule = { + id: string; + max_msg: number; + timeframe: number; + action: 0|1|2|3|4; + channels: string[] | null; + message: string | null; +} + +const ServerDashboard: FunctionComponent = () => { + const [serverInfo, setServerInfo] = useState({} as Server); + const [status, setStatus] = useState(''); + + const [changed, setChanged] = useState({} as { prefix?: boolean, prefixAllowSpace?: boolean }); + const [prefix, setPrefix] = useState('' as string|undefined); + const [prefixAllowSpace, setPrefixAllowSpace] = useState(false); + + const [botManagers, setBotManagers] = useState([] as string[]); + const [moderators, setModerators] = useState([] as string[]); + + const [automodSettings, setAutomodSettings] = useState(null as { antispam: AntispamRule[] }|null); + + const { serverid } = useParams(); + + const saveConfig = useCallback(async () => { + if (Object.values(changed).filter(i => i).length == 0) return; + + const payload = { + ...(changed.prefix ? { prefix } : undefined), + ...(changed.prefixAllowSpace ? { spaceAfterPrefix: prefixAllowSpace } : undefined), + } + + const res = await axios.put( + API_URL + `/dash/server/${serverid}/config`, + payload, + { headers: await getAuthHeaders() } + ); + + if (res.data.success) { + setChanged({}); + } + }, [ prefix, prefixAllowSpace, changed ]); + + const loadInfo = useCallback(async () => { + try { + const res = await axios.get(`${API_URL}/dash/server/${serverid}`, { headers: await getAuthHeaders() }); + console.log(res.data); + + const server: Server = res.data.server; + setServerInfo(server); + + setPrefix(server.serverConfig?.prefix || ''); + setPrefixAllowSpace(!!server.serverConfig?.spaceAfterPrefix); + + setBotManagers(server.serverConfig?.botManagers ?? []); + setModerators(server.serverConfig?.moderators ?? []); + + loadAutomodInfo(server); + } catch(e: any) { + console.error(e); + setStatus(`${e?.message ?? e}`); + } + }, [serverInfo]); + + const loadAutomodInfo = useCallback(async (server: Server) => { + if ((server.perms ?? 0) > 0) { + const res = await axios.get(API_URL + `/dash/server/${serverid}/automod`, { headers: await getAuthHeaders() }); + setAutomodSettings(res.data); + console.log(res.data); + } + }, []); + + useEffect(() => { + loadInfo(); + }, []); + + return ( + <> +

{serverInfo?.name ?? 'Loading...'}

+ {status.length ? {status} :
} + + + ); + + function RemoveButton(props: { onClick: () => void }) { + return ( +
+ +
+ ) + } + + function UserListEntry(props: { user: User, type: 'MANAGER'|'MOD' }) { + return ( +
+ + {props.user.username ?? 'Unknown'} + { + const res = await axios.delete( + `${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}/${props.user.id}`, + { headers: await getAuthHeaders() } + ); + + if (props.type == 'MANAGER') { + setBotManagers(res.data.managers); + } + else if (props.type == 'MOD') { + setModerators(res.data.mods); + } + }} + /> +
+ ); + } + + function UserListContainer(props: { disabled: boolean, children: any }) { + return ( +
+ {props.children} +
+ ); + } + + function UserListTypeContainer(props: any) { + return ( +
{props.children}
+ ); + } + + function UserListAddField(props: { type: 'MANAGER'|'MOD' }) { + const [content, setContent] = useState(''); + + const onConfirm = useCallback(async () => {0 + if (content.length) { + const res = await axios.put( + `${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}`, + { item: content }, + { headers: await getAuthHeaders() } + ); + + if (res.data.users?.length) { + res.data.users.forEach((user: User) => { + if (!serverInfo.users.find(u => u.id == user.id)) serverInfo.users.push(user); + }); + } + + if (props.type == 'MANAGER') { + setBotManagers(res.data.managers); + } + else if (props.type == 'MOD') { + setModerators(res.data.mods); + } + } + }, [content]); + + return ( +
+ setContent(e.currentTarget.value)} + style={{ + float: 'left', + width: '180px', + height: '38px', + margin: '4px 8px', + }} + onKeyDown={e => e.key == 'Enter' && onConfirm()} + /> + +
+ ); + } + + function ChannelListAddField(props: { onInput: (channel: Channel) => void }) { + const [content, setContent] = useState(''); + + const onConfirm = useCallback(async () => { + if (content.length) { + const channel = serverInfo.channels + .find(c => c.id == content.toUpperCase()) + || serverInfo.channels + .find(c => c.name == content) + || serverInfo.channels // Prefer channel with same capitalization, + .find(c => c.name.toLowerCase() == content.toLowerCase()); // otherwise search case insensitive + + if (channel && channel.type == 'TEXT') { + props.onInput(channel); + setContent(''); + } + } + }, [content]); + + return ( +
+ setContent(e.currentTarget.value)} + style={{ + float: 'left', + width: '180px', + height: '38px', + margin: '4px 8px', + }} + onKeyDown={e => e.key == 'Enter' && onConfirm()} + /> + +
+ ); + } + + function AntispamRule(props: { rule: AntispamRule }) { + const [maxMsg, setMaxMsg] = useState(props.rule.max_msg); + const [timeframe, setTimeframe] = useState(props.rule.timeframe); + const [action, setAction] = useState(props.rule.action); + const [message, setMessage] = useState(props.rule.message || ''); + const [channels, setChannels] = useState(props.rule.channels ?? []); + const [channelsChanged, setChannelsChanged] = useState(false); + + const save = useCallback(async () => { + await axios.patch( + `${API_URL}/dash/server/${serverid}/automod/${props.rule.id}`, + { + action: action != props.rule.action ? action : undefined, + channels: channelsChanged ? channels : undefined, + max_msg: maxMsg != props.rule.max_msg ? maxMsg : undefined, + message: message != props.rule.message ? message : undefined, + timeframe: timeframe != props.rule.timeframe ? timeframe : undefined, + } as AntispamRule, + { headers: await getAuthHeaders() } + ); + + await loadAutomodInfo(serverInfo); + }, [maxMsg, timeframe, action, message, channels, channelsChanged]); + + const reset = useCallback(() => { + setMaxMsg(props.rule.max_msg); + setTimeframe(props.rule.timeframe); + setAction(props.rule.action); + setMessage(props.rule.message || ''); + setChannels(props.rule.channels ?? []); + setChannelsChanged(false); + }, []); + + const inputStyle: React.CSSProperties = { + maxWidth: '100px', + margin: '8px 8px 0px 8px', + } + + const messagePlaceholders = { + 0: '', + 1: 'Message content...', + 2: '(Optional) Warn reason...', + 3: '', + 4: '', + } + + return ( +
+ +
+ If user sends more than + { + const val = e.currentTarget.value; + if (!isNaN(Number(val)) && val.length <= 4 && Number(val) >= 0) setMaxMsg(Number(val)); + }} /> + messages in + { + const val = e.currentTarget.value; + if (!isNaN(Number(val)) && val.length <= 4 && Number(val) >= 0) setTimeframe(Number(val)); + }} /> + seconds, + setAction(ev.currentTarget.value as any)} + > + + + + + + + = 3 || action == 0 ? 'none' : 'unset' }} + value={message} + placeholder={messagePlaceholders[action] || ''} + onChange={ev => setMessage(ev.currentTarget.value)} + /> + = 3 ? 'unset' : 'none'}}> +
+ "Kick" and "Ban" actions are currently placeholders, they do not have any functionality yet. +
+ +

+ You can specify channels here that this rule will run in. + If left empty, it will run in all channels. +

+ + { + channels.map(cid => { + const channel: Channel = serverInfo.channels.find(c => c.id == cid && c.type == 'TEXT') + || { id: cid, name: 'Unknown channel', nsfw: false, type: 'TEXT' }; + return ( +
+ + {channel.name} + { + setChannels(channels.filter(c => c != cid)); + setChannelsChanged(true); + }} /> +
+ ) + }) + } + { + if (!channels.includes(channel.id)) { + setChannels([ ...channels, channel.id ]); + setChannelsChanged(true); + } + }} /> +
+
+
+
+ + +
+
+
+ ) + } +} +export default ServerDashboard; diff --git a/web/src/utils.ts b/web/src/utils.ts new file mode 100644 index 0000000..7b6b1e7 --- /dev/null +++ b/web/src/utils.ts @@ -0,0 +1,30 @@ +import axios from "axios"; +import localforage from "localforage"; +import { API_URL } from "./App"; + +async function getAuthHeaders() { + const auth: any = await localforage.getItem('auth'); + return { + 'x-auth-user': auth.user, + 'x-auth-token': auth.token, + } +} + +async function getAuth(): Promise { + const auth: any = await localforage.getItem('auth'); + if (!auth) return false; + + try { + const res = await axios.get(API_URL, { + headers: { + 'x-auth-user': auth.user, + 'x-auth-token': auth.token, + } + }); + + if (res.data?.authenticated) return { user: auth.user ?? '', token: auth.token ?? '' } + else return false; + } catch(e) { return false } // todo: dont assume we're logged out if death +} + +export { getAuth, getAuthHeaders } \ No newline at end of file diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..9f83659 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["./src"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..b1b5f91 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +}) diff --git a/web/yarn.lock b/web/yarn.lock new file mode 100644 index 0000000..5c07b91 --- /dev/null +++ b/web/yarn.lock @@ -0,0 +1,946 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.16.4": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.8.tgz#31560f9f29fdf1868de8cb55049538a1b9732a60" + integrity sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q== + +"@babel/core@^7.16.5": + version "7.16.12" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.12.tgz#5edc53c1b71e54881315923ae2aedea2522bb784" + integrity sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.16.7" + "@babel/parser" "^7.16.12" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.10" + "@babel/types" "^7.16.8" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + +"@babel/generator@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.8.tgz#359d44d966b8cd059d543250ce79596f792f2ebe" + integrity sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw== + dependencies: + "@babel/types" "^7.16.8" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.16.0", "@babel/helper-annotate-as-pure@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" + integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-compilation-targets@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" + integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== + dependencies: + "@babel/compat-data" "^7.16.4" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.17.5" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" + integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== + dependencies: + "@babel/helper-get-function-arity" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-get-function-arity@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" + integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-transforms@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz#7665faeb721a01ca5327ddc6bba15a5cb34b6a41" + integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-plugin-utils@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" + integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== + +"@babel/helper-simple-access@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" + integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helpers@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.7.tgz#7e3504d708d50344112767c3542fc5e357fffefc" + integrity sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/highlight@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.16.10", "@babel/parser@^7.16.12", "@babel/parser@^7.16.7": + version "7.16.12" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.12.tgz#9474794f9a650cf5e2f892444227f98e28cdf8b6" + integrity sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A== + +"@babel/plugin-syntax-jsx@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665" + integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-jsx-development@^7.16.5": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz#43a00724a3ed2557ed3f276a01a929e6686ac7b8" + integrity sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.16.7" + +"@babel/plugin-transform-react-jsx-self@^7.16.5": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.16.7.tgz#f432ad0cba14c4a1faf44f0076c69e42a4d4479e" + integrity sha512-oe5VuWs7J9ilH3BCCApGoYjHoSO48vkjX2CbA5bFVhIuO2HKxA3vyF7rleA4o6/4rTDbk6r8hBW7Ul8E+UZrpA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-jsx-source@^7.16.5": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz#1879c3f23629d287cc6186a6c683154509ec70c0" + integrity sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-jsx@^7.16.5", "@babel/plugin-transform-react-jsx@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.16.7.tgz#86a6a220552afd0e4e1f0388a68a372be7add0d4" + integrity sha512-8D16ye66fxiE8m890w0BpPpngG9o9OVBBy0gH2E+2AR7qMR2ZpTYJEqLxAsoroenMId0p/wMW+Blc0meDgu0Ag== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-jsx" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/runtime@^7.10.5", "@babel/runtime@^7.15.3", "@babel/runtime@^7.7.6": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.16.10", "@babel/traverse@^7.16.7", "@babel/traverse@^7.4.5": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.10.tgz#448f940defbe95b5a8029975b051f75993e8239f" + integrity sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.16.10" + "@babel/types" "^7.16.8" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.16.7", "@babel/types@^7.16.8": + version "7.16.8" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.8.tgz#0ba5da91dd71e0a4e7781a30f22770831062e3c1" + integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@emotion/is-prop-valid@^0.8.7", "@emotion/is-prop-valid@^0.8.8": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" + integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== + dependencies: + "@emotion/memoize" "0.7.4" + +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + +"@emotion/stylis@^0.8.4": + version "0.8.5" + resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04" + integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ== + +"@emotion/unitless@^0.7.4": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@mdi/js@^6.5.95": + version "6.5.95" + resolved "https://registry.yarnpkg.com/@mdi/js/-/js-6.5.95.tgz#2d895b013408f213252b77c30e0fdaaba6dc8b4b" + integrity sha512-x/bwEoAGP+Mo10Dfk5audNIPi7Yz8ZBrILcbXLW3ShOI/njpgodzpgpC2WYK3D2ZSC392peRRemIFb/JsyzzYQ== + +"@mdi/react@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@mdi/react/-/react-1.5.0.tgz#461d2064ba12d509723bffc95e2f7169a6ac884a" + integrity sha512-NztRgUxSYD+ImaKN94Tg66VVVqXj4SmlDGzZoz48H9riJ+Awha56sfXH2fegw819NWo7KI3oeS1Es0lNQqwr0w== + +"@revoltchat/ui@^1.0.24": + version "1.0.24" + resolved "https://registry.yarnpkg.com/@revoltchat/ui/-/ui-1.0.24.tgz#ce55cc225ec92eb07dd5865b9255d1f160d06ce2" + integrity sha512-3CPnfWtEN0IE5ERy4QBhLzjQGGPE0OKLlFrBw7VZzuaQDoAsUo9/CjHnCY1Cc0HQlT6scbrNXyvInjOKggds1g== + dependencies: + "@styled-icons/boxicons-logos" "^10.38.0" + "@styled-icons/boxicons-regular" "^10.38.0" + "@styled-icons/boxicons-solid" "^10.38.0" + +"@rollup/pluginutils@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.2.tgz#ed5821c15e5e05e32816f5fb9ec607cdf5a75751" + integrity sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@styled-icons/boxicons-logos@^10.38.0": + version "10.38.0" + resolved "https://registry.yarnpkg.com/@styled-icons/boxicons-logos/-/boxicons-logos-10.38.0.tgz#f51442c49f1a28c61927d6d2f8c1e2d7946faf46" + integrity sha512-DpkUi9qV76RsyuzihQhfCho142WCxK/2oUfjcqo6BsP92qxV+4rtS5nfVsmqfnSEef4av+qSeg33fJWDyURyKg== + dependencies: + "@babel/runtime" "^7.15.3" + "@styled-icons/styled-icon" "^10.6.3" + +"@styled-icons/boxicons-regular@^10.38.0": + version "10.38.0" + resolved "https://registry.yarnpkg.com/@styled-icons/boxicons-regular/-/boxicons-regular-10.38.0.tgz#1eb80b4f94a18a9b77b11dee5204aa23378d37ec" + integrity sha512-xjhafoa0/EeYtQOyTw+ohuSlBx599t8l8++JH1tQlM0l73dP4icTpF4znYL+HhZeaUe3D+UhrkfWuBBua2w2Qw== + dependencies: + "@babel/runtime" "^7.15.3" + "@styled-icons/styled-icon" "^10.6.3" + +"@styled-icons/boxicons-solid@^10.38.0": + version "10.38.0" + resolved "https://registry.yarnpkg.com/@styled-icons/boxicons-solid/-/boxicons-solid-10.38.0.tgz#bb2a6e21f3412c97ae8029396e3ca9fb107c7658" + integrity sha512-uuMtF1R61smQT5r6HIYMHvpVD0XSErXXNQYl5d6lCtMyefd0gIE5Rw+iD9i/RxHFUHNHbvL69LvitCLtNWrSqw== + dependencies: + "@babel/runtime" "^7.15.3" + "@styled-icons/styled-icon" "^10.6.3" + +"@styled-icons/styled-icon@^10.6.3": + version "10.6.3" + resolved "https://registry.yarnpkg.com/@styled-icons/styled-icon/-/styled-icon-10.6.3.tgz#eae0e5e18fd601ac47e821bb9c2e099810e86403" + integrity sha512-/A95L3peioLoWFiy+/eKRhoQ9r/oRrN/qzbSX4hXU1nGP2rUXcX3LWUhoBNAOp9Rw38ucc/4ralY427UUNtcGQ== + dependencies: + "@babel/runtime" "^7.10.5" + "@emotion/is-prop-valid" "^0.8.7" + +"@types/axios@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46" + integrity sha1-7CMA++fX3d1+udOr+HmZlkyvzkY= + dependencies: + axios "*" + +"@types/core-js@^2.5.5": + version "2.5.5" + resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.5.tgz#dc5a013ee0b23bd5ac126403854fadabb4f24f75" + integrity sha512-C4vwOHrhsvxn7UFyk4NDQNUpgNKdWsT/bL39UWyD75KSEOObZSKa9mYDOCM5FGeJG2qtbG0XiEbUKND2+j0WOg== + +"@types/hoist-non-react-statics@*": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + +"@types/prop-types@*": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + +"@types/react-dom@^17.0.10": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" + integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^17.0.33": + version "17.0.38" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" + integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +"@types/styled-components@^5.1.21": + version "5.1.21" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.21.tgz#39f6bdc4103254d899531ef099dae5619b039cdb" + integrity sha512-lQzA0T6CaLXoeiOkSe2US2JfFgJV2/yJ8W1BaJubQQh2wdq7H+qScQQfbjURyLkgI1Ig+S/jRHCrWikfMHC6zA== + dependencies: + "@types/hoist-non-react-statics" "*" + "@types/react" "*" + csstype "^3.0.2" + +"@vitejs/plugin-react@^1.0.7": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-1.1.4.tgz#a3f3f49d11890bf73bc64d545ce8fe7f5d506e85" + integrity sha512-cMUBDonNY8PPeHWjIrYKbRn6bLSunh/Ixo2XLLBd3DM0uYBZft+c+04zkGhhN1lAwvoRKJ2FdtvhGhPgViHc6w== + dependencies: + "@babel/core" "^7.16.5" + "@babel/plugin-transform-react-jsx" "^7.16.5" + "@babel/plugin-transform-react-jsx-development" "^7.16.5" + "@babel/plugin-transform-react-jsx-self" "^7.16.5" + "@babel/plugin-transform-react-jsx-source" "^7.16.5" + "@rollup/pluginutils" "^4.1.2" + react-refresh "^0.11.0" + resolve "^1.20.0" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +axios@*, axios@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" + integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== + dependencies: + follow-redirects "^1.14.7" + +"babel-plugin-styled-components@>= 1.12.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz#0fac11402dc9db73698b55847ab1dc73f5197c54" + integrity sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.0" + "@babel/helper-module-imports" "^7.16.0" + babel-plugin-syntax-jsx "^6.18.0" + lodash "^4.17.11" + +babel-plugin-syntax-jsx@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946" + integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY= + +browserslist@^4.17.5: + version "4.19.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" + integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== + dependencies: + caniuse-lite "^1.0.30001286" + electron-to-chromium "^1.4.17" + escalade "^3.1.1" + node-releases "^2.0.1" + picocolors "^1.0.0" + +camelize@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= + +caniuse-lite@^1.0.30001286: + version "1.0.30001301" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001301.tgz#ebc9086026534cab0dab99425d9c3b4425e5f450" + integrity sha512-csfD/GpHMqgEL3V3uIgosvh+SVIQvCh43SNu9HRbP1lnxkKm1kjDG4f32PP571JplkLjfS+mg2p1gxR7MYrrIA== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +core-js@^3.20.3: + version "3.20.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.3.tgz#c710d0a676e684522f3db4ee84e5e18a9d11d69a" + integrity sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag== + +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= + +css-to-react-native@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.0.0.tgz#62dbe678072a824a689bcfee011fc96e02a7d756" + integrity sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + +csstype@^3.0.2: + version "3.0.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" + integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== + +debug@^4.1.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +electron-to-chromium@^1.4.17: + version "1.4.51" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.51.tgz#a432f5a5d983ace79278a33057300cf949627e63" + integrity sha512-JNEmcYl3mk1tGQmy0EvL5eik/CKSBuzAyGP0QFdG6LIgxQe3II0BL1m2zKc2MZMf3uGqHWE1TFddJML0RpjSHQ== + +esbuild-android-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44" + integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg== + +esbuild-darwin-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72" + integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ== + +esbuild-darwin-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a" + integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ== + +esbuild-freebsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85" + integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA== + +esbuild-freebsd-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52" + integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ== + +esbuild-linux-32@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69" + integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g== + +esbuild-linux-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3" + integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA== + +esbuild-linux-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1" + integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA== + +esbuild-linux-arm@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe" + integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA== + +esbuild-linux-mips64le@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7" + integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg== + +esbuild-linux-ppc64le@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2" + integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ== + +esbuild-netbsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038" + integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w== + +esbuild-openbsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7" + integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g== + +esbuild-sunos-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4" + integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw== + +esbuild-windows-32@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7" + integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw== + +esbuild-windows-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294" + integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ== + +esbuild-windows-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3" + integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA== + +esbuild@^0.13.12: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf" + integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw== + optionalDependencies: + esbuild-android-arm64 "0.13.15" + esbuild-darwin-64 "0.13.15" + esbuild-darwin-arm64 "0.13.15" + esbuild-freebsd-64 "0.13.15" + esbuild-freebsd-arm64 "0.13.15" + esbuild-linux-32 "0.13.15" + esbuild-linux-64 "0.13.15" + esbuild-linux-arm "0.13.15" + esbuild-linux-arm64 "0.13.15" + esbuild-linux-mips64le "0.13.15" + esbuild-linux-ppc64le "0.13.15" + esbuild-netbsd-64 "0.13.15" + esbuild-openbsd-64 "0.13.15" + esbuild-sunos-64 "0.13.15" + esbuild-windows-32 "0.13.15" + esbuild-windows-64 "0.13.15" + esbuild-windows-arm64 "0.13.15" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +follow-redirects@^1.14.7: + version "1.14.7" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" + integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +history@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/history/-/history-5.2.0.tgz#7cdd31cf9bac3c5d31f09c231c9928fad0007b7c" + integrity sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig== + dependencies: + "@babel/runtime" "^7.7.6" + +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + +is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json5@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== + dependencies: + minimist "^1.2.5" + +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= + dependencies: + immediate "~3.0.5" + +localforage@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + +lodash@^4.17.11: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@^3.1.30: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== + +node-releases@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" + integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.2.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss-value-parser@^4.0.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.5: + version "8.4.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" + integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== + dependencies: + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^1.0.1" + +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-refresh@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" + integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== + +react-router-dom@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec" + integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA== + dependencies: + history "^5.2.0" + react-router "6.2.1" + +react-router@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.1.tgz#be2a97a6006ce1d9123c28934e604faef51448a3" + integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg== + dependencies: + history "^5.2.0" + +react@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +resolve@^1.20.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^2.59.0: + version "2.66.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.66.0.tgz#ee529ea15a20485d579039637fec3050bad03bbb" + integrity sha512-L6mKOkdyP8HK5kKJXaiWG7KZDumPJjuo1P+cfyHOJPNNTK3Moe7zCH5+fy7v8pVmHXtlxorzaBjvkBMB23s98g== + optionalDependencies: + fsevents "~2.3.2" + +safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +source-map-js@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.5.0: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +styled-components@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.3.tgz#312a3d9a549f4708f0fb0edc829eb34bde032743" + integrity sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/traverse" "^7.4.5" + "@emotion/is-prop-valid" "^0.8.8" + "@emotion/stylis" "^0.8.4" + "@emotion/unitless" "^0.7.4" + babel-plugin-styled-components ">= 1.12.0" + css-to-react-native "^3.0.0" + hoist-non-react-statics "^3.0.0" + shallowequal "^1.1.0" + supports-color "^5.5.0" + +supports-color@^5.3.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +typescript@^4.4.4: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + +vite@^2.7.2: + version "2.7.13" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.7.13.tgz#99b56e27dfb1e4399e407cf94648f5c7fb9d77f5" + integrity sha512-Mq8et7f3aK0SgSxjDNfOAimZGW9XryfHRa/uV0jseQSilg+KhYDSoNb9h1rknOy6SuMkvNDLKCYAYYUMCE+IgQ== + dependencies: + esbuild "^0.13.12" + postcss "^8.4.5" + resolve "^1.20.0" + rollup "^2.59.0" + optionalDependencies: + fsevents "~2.3.2"