mirror of
https://github.com/janderedev/automod.git
synced 2024-12-22 10:45:27 +00:00
Merge pull request #3 from janderedev/web
API and Web UI for managing server configuration
This commit is contained in:
commit
7fbd0704cd
39
.env.example
Normal file
39
.env.example
Normal file
|
@ -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
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -2,3 +2,9 @@ node_modules
|
|||
dist
|
||||
.env
|
||||
yarn-error.log
|
||||
docker-compose.yml
|
||||
/db
|
||||
.vercel
|
||||
|
||||
# For stuff like docker mounts
|
||||
/private
|
||||
|
|
|
@ -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
|
28
api/package.json
Normal file
28
api/package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
29
api/src/db.ts
Normal file
29
api/src/db.ts
Normal file
|
@ -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;
|
||||
}
|
42
api/src/index.ts
Normal file
42
api/src/index.ts
Normal file
|
@ -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');
|
9
api/src/middlewares/cors.ts
Normal file
9
api/src/middlewares/cors.ts
Normal file
|
@ -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();
|
||||
});
|
7
api/src/middlewares/log.ts
Normal file
7
api/src/middlewares/log.ts
Normal file
|
@ -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();
|
||||
});
|
109
api/src/middlewares/ratelimit.ts
Normal file
109
api/src/middlewares/ratelimit.ts
Normal file
|
@ -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<RateLimitObject> = 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 }
|
19
api/src/middlewares/updateTokenExpiry.ts
Normal file
19
api/src/middlewares/updateTokenExpiry.ts
Normal file
|
@ -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<any> = 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) }
|
||||
});
|
92
api/src/routes/dash/server-automod.ts
Normal file
92
api/src/routes/dash/server-automod.ts
Normal file
|
@ -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<any> = 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<any> = 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 });
|
||||
});
|
195
api/src/routes/dash/server.ts
Normal file
195
api/src/routes/dash/server.ts
Normal file
|
@ -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);
|
||||
}
|
||||
});
|
21
api/src/routes/dash/servers.ts
Normal file
21
api/src/routes/dash/servers.ts
Normal file
|
@ -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 });
|
||||
});
|
87
api/src/routes/internal/ws.ts
Normal file
87
api/src/routes/internal/ws.ts
Normal file
|
@ -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<botReqRes> {
|
||||
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 }
|
68
api/src/routes/login.ts
Normal file
68
api/src/routes/login.ts
Normal file
|
@ -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<any> = 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 });
|
||||
});
|
11
api/src/routes/root.ts
Normal file
11
api/src/routes/root.ts
Normal file
|
@ -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')!) : {},
|
||||
});
|
||||
});
|
5
api/src/server.ts
Normal file
5
api/src/server.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { app, logger, PORT } from ".";
|
||||
|
||||
const server = app.listen(PORT, () => logger.info(`Listening on port ${PORT}`));
|
||||
|
||||
export default server;
|
52
api/src/utils.ts
Normal file
52
api/src/utils.ts
Normal file
|
@ -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<string|false> {
|
||||
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<SessionInfo> {
|
||||
const session: FindOneResult<Session> = 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 }
|
665
api/yarn.lock
Normal file
665
api/yarn.lock
Normal file
|
@ -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==
|
13
bot/Dockerfile
Normal file
13
bot/Dockerfile
Normal file
|
@ -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"]
|
|
@ -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"
|
||||
},
|
|
@ -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';
|
|
@ -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(', '));
|
||||
|
|
@ -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";
|
|
@ -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";
|
|
@ -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";
|
62
bot/src/bot/commands/login.ts
Normal file
62
bot/src/bot/commands/login.ts
Normal file
|
@ -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<PendingLogin> = 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;
|
64
bot/src/bot/commands/logout.ts
Normal file
64
bot/src/bot/commands/logout.ts
Normal file
|
@ -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<any>[] = 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<PendingLogin> = 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<any> = 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;
|
|
@ -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';
|
|
@ -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";
|
||||
|
|
@ -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";
|
|
@ -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";
|
|
@ -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";
|
|
@ -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";
|
||||
|
|
@ -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;
|
|
@ -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";
|
|
@ -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";
|
92
bot/src/bot/modules/api/server_details.ts
Normal file
92
bot/src/bot/modules/api/server_details.ts
Normal file
|
@ -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 }
|
54
bot/src/bot/modules/api/servers.ts
Normal file
54
bot/src/bot/modules/api/servers.ts
Normal file
|
@ -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<ServerResponse>[] = [];
|
||||
|
||||
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}` });
|
||||
}
|
||||
});
|
38
bot/src/bot/modules/api/users.ts
Normal file
38
bot/src/bot/modules/api/users.ts
Normal file
|
@ -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}` });
|
||||
}
|
||||
});
|
143
bot/src/bot/modules/api_communication.ts
Normal file
143
bot/src/bot/modules/api_communication.ts
Normal file
|
@ -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');
|
|
@ -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}`);
|
||||
|
|
@ -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) {
|
|
@ -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";
|
||||
|
|
@ -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";
|
|
@ -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";
|
|
@ -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';
|
|
@ -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";
|
|
@ -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";
|
|
@ -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<boolean> {
|
|||
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,
|
|
@ -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');
|
||||
})();
|
|
@ -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";
|
|
@ -1,4 +1,4 @@
|
|||
import { ChannelPermission, ServerPermission } from "revolt.js";
|
||||
import { ChannelPermission, ServerPermission } from "@janderedev/revolt.js";
|
||||
|
||||
class Command {
|
||||
name: string;
|
|
@ -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 {
|
12
bot/src/struct/PendingLogin.ts
Normal file
12
bot/src/struct/PendingLogin.ts
Normal file
|
@ -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;
|
100
bot/tsconfig.json
Normal file
100
bot/tsconfig.json
Normal file
|
@ -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 `<reference>`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. */
|
||||
}
|
||||
}
|
|
@ -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"
|
65
docker-compose.yml.example
Normal file
65
docker-compose.yml.example
Normal file
|
@ -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
|
|
@ -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<typeof Message.prototype.reply>) => {
|
||||
return new Promise<Message>((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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
5
web/.gitignore
vendored
Normal file
5
web/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
16
web/Dockerfile
Normal file
16
web/Dockerfile
Normal file
|
@ -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"]
|
13
web/index.html
Normal file
13
web/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
32
web/package.json
Normal file
32
web/package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
42
web/src/App.css
Normal file
42
web/src/App.css
Normal file
|
@ -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);
|
||||
}
|
29
web/src/App.tsx
Normal file
29
web/src/App.tsx
Normal file
|
@ -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 (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path='/' element={<Home />} />
|
||||
<Route path='/dashboard' element={<RequireAuth><DashboardHome /></RequireAuth>} />
|
||||
<Route path='/dashboard/:serverid' element={<RequireAuth><ServerDashboard /></RequireAuth>} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export { API_URL, BOT_PREFIX }
|
1
web/src/assets/channel-default-icon.svg
Normal file
1
web/src/assets/channel-default-icon.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" height="24" width="24" aria-hidden="true" focusable="false" fill="#848484" xmlns="http://www.w3.org/2000/svg" class="StyledIconBase-ea9ulj-0 bWRyML"><path d="M16.018 3.815 15.232 8h-4.966l.716-3.815-1.964-.37L8.232 8H4v2h3.857l-.751 4H3v2h3.731l-.714 3.805 1.965.369L8.766 16h4.966l-.714 3.805 1.965.369.783-4.174H20v-2h-3.859l.751-4H21V8h-3.733l.716-3.815-1.965-.37zM14.106 14H9.141l.751-4h4.966l-.752 4z"></path></svg>
|
After Width: | Height: | Size: 445 B |
15
web/src/components/RequireAuth.tsx
Normal file
15
web/src/components/RequireAuth.tsx
Normal file
|
@ -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}</> : <Login />
|
||||
}
|
||||
|
||||
export default RequireAuth;
|
13
web/src/index.css
Normal file
13
web/src/index.css
Normal file
|
@ -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;
|
||||
}
|
11
web/src/main.tsx
Normal file
11
web/src/main.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
82
web/src/pages/DashboardHome.tsx
Normal file
82
web/src/pages/DashboardHome.tsx
Normal file
|
@ -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 (
|
||||
<div>
|
||||
<H1>dashbord</H1>
|
||||
<br/>
|
||||
<p hidden={!loading}>loading</p>
|
||||
{
|
||||
servers.map(server => <div className="server-card" style={{ paddingTop: '10px' }} key={server.id}>
|
||||
<img
|
||||
src={server.iconURL ?? 'https://dl.insrt.uk/projects/revolt/emotes/trol.png'}
|
||||
width={48}
|
||||
height={48}
|
||||
style={{
|
||||
float: 'left',
|
||||
marginLeft: '8px',
|
||||
marginRight: '12px',
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
float: 'left',
|
||||
maxWidth: '240px',
|
||||
textOverflow: 'clip',
|
||||
overflow: 'clip',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
<H2>{server.name} ({permissionName(server.perms)})</H2>
|
||||
<code style={{ color: 'var(--foreground)' }}>{server.id}</code>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
style={{ position: 'relative', top: '8px', left: '12px' }}
|
||||
onClick={() => {
|
||||
navigate(`/dashboard/${server.id}`);
|
||||
}}
|
||||
>Open</Button>
|
||||
</div>
|
||||
<div style={{ clear: 'both' }} />
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
12
web/src/pages/Home.tsx
Normal file
12
web/src/pages/Home.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { FunctionComponent } from "react";
|
||||
|
||||
const Home: FunctionComponent = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>todo: web design</h1>
|
||||
<a href='/dashboard'>sign in</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
79
web/src/pages/Login.tsx
Normal file
79
web/src/pages/Login.tsx
Normal file
|
@ -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 (
|
||||
<div>
|
||||
<H1>log in</H1>
|
||||
{statusMsg.length ? <a>{statusMsg}</a> : <br/>}
|
||||
<div hidden={!showInitial}>
|
||||
<InputBox
|
||||
onChange={e => {
|
||||
setUsername(e.target.value);
|
||||
setStatusMsg('');
|
||||
}}
|
||||
placeholder="Enter your user ID..."
|
||||
style={{ width: "200px", float: "left" }}
|
||||
/>
|
||||
<Button onClick={getCode} disabled={username.length == 0}>Continue</Button>
|
||||
</div>
|
||||
<div hidden={!showSecond}>
|
||||
<H2>Your code: <a>{code}</a></H2>
|
||||
<p style={{ color: "var(--foreground)" }}>
|
||||
Run <code style={{ userSelect: 'all' }}>
|
||||
{BOT_PREFIX}login {code}
|
||||
</code> in any server using AutoMod, then <a
|
||||
onClick={getSession}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>click here</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
569
web/src/pages/ServerDashboard.tsx
Normal file
569
web/src/pages/ServerDashboard.tsx
Normal file
|
@ -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 (
|
||||
<>
|
||||
<H1>{serverInfo?.name ?? 'Loading...'}</H1>
|
||||
{status.length ? <a>{status}</a> : <br/>}
|
||||
<div hidden={Object.keys(serverInfo).length == 0}>
|
||||
<H4>{serverInfo.description ?? <i>No server description set</i>}</H4>
|
||||
<br/>
|
||||
<div style={{ paddingLeft: '10px', paddingRight: '10px' }}>
|
||||
<>
|
||||
<H3>Prefix</H3>
|
||||
<InputBox
|
||||
style={{ width: '150px', }}
|
||||
placeholder="Enter a prefix..."
|
||||
value={prefix}
|
||||
onChange={e => {
|
||||
setPrefix(e.currentTarget.value);
|
||||
setChanged({ ...changed, prefix: true });
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
style={{ maxWidth: '400px' }}
|
||||
value={prefixAllowSpace}
|
||||
onChange={() => {
|
||||
setPrefixAllowSpace(!prefixAllowSpace);
|
||||
setChanged({ ...changed, prefixAllowSpace: true });
|
||||
}}
|
||||
title="Allow space after prefix"
|
||||
description={'Whether the bot recognizes a command if the prefix is followed by a space. Enable if your prefix is a word.'}
|
||||
/>
|
||||
<Button
|
||||
style={{ marginTop: "16px" }}
|
||||
onClick={saveConfig}
|
||||
>Save</Button>
|
||||
</>
|
||||
|
||||
<LineDivider />
|
||||
|
||||
<>
|
||||
<H3>Bot Managers</H3>
|
||||
<H4>
|
||||
Only users with "Manage Server" permission are allowed to add/remove other
|
||||
bot managers and are automatically considered bot manager.
|
||||
</H4>
|
||||
<UserListTypeContainer>
|
||||
<UserListContainer disabled={(serverInfo.perms ?? 0) < 3}>
|
||||
{botManagers.map((uid: string) => {
|
||||
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
|
||||
return (
|
||||
<UserListEntry type='MANAGER' user={user} key={uid} />
|
||||
)})}
|
||||
<UserListAddField type='MANAGER' />
|
||||
</UserListContainer>
|
||||
</UserListTypeContainer>
|
||||
|
||||
<H3>Moderators</H3>
|
||||
<H4>
|
||||
Only bot managers are allowed to add/remove moderators.
|
||||
All bot managers are also moderators.
|
||||
</H4>
|
||||
<UserListTypeContainer>
|
||||
<UserListContainer disabled={(serverInfo.perms ?? 0) < 2}>
|
||||
{moderators.map((uid: string) => {
|
||||
const user = serverInfo.users.find(u => u.id == uid) || { id: uid }
|
||||
return (
|
||||
<UserListEntry type='MOD' user={user} key={uid} />
|
||||
)})}
|
||||
<UserListAddField type='MOD' />
|
||||
</UserListContainer>
|
||||
</UserListTypeContainer>
|
||||
</>
|
||||
|
||||
<LineDivider />
|
||||
|
||||
<>
|
||||
<H3>Antispam Rules</H3>
|
||||
{serverInfo.perms != null && automodSettings && (
|
||||
serverInfo.perms > 0
|
||||
? (
|
||||
<>
|
||||
{automodSettings.antispam.map(r => <AntispamRule rule={r} key={r.id} />)}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
<p style={{ color: 'var(--foreground)' }}>
|
||||
You do not have access to this.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
function RemoveButton(props: { onClick: () => void }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
height: '30px',
|
||||
}}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Icon // todo: hover effect
|
||||
path={mdiCloseBox}
|
||||
color='var(--tertiary-foreground)'
|
||||
size='30px'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UserListEntry(props: { user: User, type: 'MANAGER'|'MOD' }) {
|
||||
return (
|
||||
<div
|
||||
key={props.user.id}
|
||||
style={{
|
||||
display: 'block',
|
||||
margin: '4px 6px',
|
||||
padding: '4px',
|
||||
backgroundColor: 'var(--tertiary-background)',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={props.user.avatarURL ?? 'https://amogus.org/amogus.png'}
|
||||
width={28}
|
||||
height={28}
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--foreground)',
|
||||
fontSize: '20px',
|
||||
paddingLeft: '6px',
|
||||
marginBottom: '2px',
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>{props.user.username ?? 'Unknown'}</span>
|
||||
<RemoveButton
|
||||
onClick={async () => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserListContainer(props: { disabled: boolean, children: any }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
...(props.disabled ? {
|
||||
filter: 'grayscale(100%)',
|
||||
pointerEvents: 'none',
|
||||
} : {})
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserListTypeContainer(props: any) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
backgroundColor: 'var(--secondary-background)',
|
||||
borderRadius: '10px',
|
||||
marginTop: '15px',
|
||||
paddingTop: '5px',
|
||||
paddingBottom: '5px',
|
||||
}}
|
||||
>{props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<InputBox
|
||||
placeholder={`Add a ${props.type == 'MANAGER' ? 'bot manager' : 'moderator'}...`}
|
||||
value={content}
|
||||
onChange={e => setContent(e.currentTarget.value)}
|
||||
style={{
|
||||
float: 'left',
|
||||
width: '180px',
|
||||
height: '38px',
|
||||
margin: '4px 8px',
|
||||
}}
|
||||
onKeyDown={e => e.key == 'Enter' && onConfirm()}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
float: 'left',
|
||||
width: '40px',
|
||||
height: '38px',
|
||||
margin: '4px 8px',
|
||||
opacity: content.length > 0 ? '1' : '0',
|
||||
}}
|
||||
onClick={onConfirm}
|
||||
>Ok</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<InputBox
|
||||
placeholder={`Add a channel...`}
|
||||
value={content}
|
||||
onChange={e => setContent(e.currentTarget.value)}
|
||||
style={{
|
||||
float: 'left',
|
||||
width: '180px',
|
||||
height: '38px',
|
||||
margin: '4px 8px',
|
||||
}}
|
||||
onKeyDown={e => e.key == 'Enter' && onConfirm()}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
float: 'left',
|
||||
width: '40px',
|
||||
height: '38px',
|
||||
margin: '4px 8px',
|
||||
opacity: content.length > 0 ? '1' : '0',
|
||||
}}
|
||||
onClick={onConfirm}
|
||||
>Ok</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--foreground)',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
If user sends more than
|
||||
<InputBox style={inputStyle} value={maxMsg || ''} placeholder={`${props.rule.max_msg}`} onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
if (!isNaN(Number(val)) && val.length <= 4 && Number(val) >= 0) setMaxMsg(Number(val));
|
||||
}} />
|
||||
messages in
|
||||
<InputBox style={inputStyle} value={timeframe || ''} placeholder={`${props.rule.timeframe}`} onChange={e => {
|
||||
const val = e.currentTarget.value;
|
||||
if (!isNaN(Number(val)) && val.length <= 4 && Number(val) >= 0) setTimeframe(Number(val));
|
||||
}} />
|
||||
seconds,
|
||||
<ComboBox
|
||||
style={{ ...inputStyle, maxWidth: '200px' }}
|
||||
value={action}
|
||||
onChange={ev => setAction(ev.currentTarget.value as any)}
|
||||
>
|
||||
<option value={0}>Delete message</option>
|
||||
<option value={1}>Send a message</option>
|
||||
<option value={2}>Warn user</option>
|
||||
<option value={3}>Kick user</option>
|
||||
<option value={4}>Ban user</option>
|
||||
</ComboBox>
|
||||
<InputBox
|
||||
style={{
|
||||
...inputStyle,
|
||||
maxWidth: 'min(400px, calc(100% - 20px))',
|
||||
display: action >= 3 || action == 0 ? 'none' : 'unset' }}
|
||||
value={message}
|
||||
placeholder={messagePlaceholders[action] || ''}
|
||||
onChange={ev => setMessage(ev.currentTarget.value)}
|
||||
/>
|
||||
<a style={{ display: action >= 3 ? 'unset' : 'none'}}>
|
||||
<br/>
|
||||
"Kick" and "Ban" actions are currently placeholders, they do not have any functionality yet.
|
||||
</a>
|
||||
|
||||
<H4 style={{ paddingTop: '16px' }}>
|
||||
You can specify channels here that this rule will run in.
|
||||
If left empty, it will run in all channels.
|
||||
</H4>
|
||||
<UserListTypeContainer>
|
||||
{
|
||||
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 (
|
||||
<div
|
||||
key={cid}
|
||||
style={{
|
||||
display: 'block',
|
||||
margin: '4px 6px',
|
||||
padding: '4px',
|
||||
backgroundColor: 'var(--tertiary-background)',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={channel.icon ?? defaultChannelIcon}
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '10%',
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '20px',
|
||||
verticalAlign: 'middle',
|
||||
marginLeft: '4px',
|
||||
}}
|
||||
>{channel.name}</span>
|
||||
<RemoveButton onClick={() => {
|
||||
setChannels(channels.filter(c => c != cid));
|
||||
setChannelsChanged(true);
|
||||
}} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
<ChannelListAddField onInput={channel => {
|
||||
if (!channels.includes(channel.id)) {
|
||||
setChannels([ ...channels, channel.id ]);
|
||||
setChannelsChanged(true);
|
||||
}
|
||||
}} />
|
||||
</UserListTypeContainer>
|
||||
</div>
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: '16px'
|
||||
}}
|
||||
>
|
||||
<Button style={{ float: 'left' }} onClick={save}>Save</Button>
|
||||
<Button style={{ float: 'left', marginLeft: '8px' }} onClick={reset}>Reset</Button>
|
||||
<div style={{ clear: 'both' }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default ServerDashboard;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue