add antispam settings to dashboard, various fixes

also switched to revolt.js fork
This commit is contained in:
JandereDev 2022-02-05 15:59:45 +01:00
parent dd145db89d
commit 860b816136
Signed by: Lea
GPG key ID: 5D5E18ACB990F57A
40 changed files with 526 additions and 172 deletions

View file

@ -18,7 +18,7 @@ app.use(Express.json());
export { logger, app, db, PORT, SESSION_LIFETIME }
(async () => {
await Promise.all([
const promises = [
import('./middlewares/log'),
import('./middlewares/updateTokenExpiry'),
import('./middlewares/cors'),
@ -27,7 +27,12 @@ export { logger, app, db, PORT, SESSION_LIFETIME }
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');
})();

View file

@ -1,5 +1,5 @@
import { Request, Response } from "express";
import { app, logger } from "..";
import { app } from "..";
app.use('*', (req: Request, res: Response, next: () => void) => {
res.header('Access-Control-Allow-Origin', '*');

View 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 });
});

View file

@ -4,6 +4,7 @@ import { badRequest, getPermissionLevel, isAuthenticated, unauthorized } from '.
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,
@ -14,11 +15,12 @@ type ServerDetails = {
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 unauthorized(res);
if (!user) return;
const { server } = req.params;
if (!server || typeof server != 'string') return badRequest(res);

View file

@ -37,12 +37,12 @@ async function getSessionInfo(user: string, token: string): Promise<SessionInfo>
return { exists: !!session, valid: !!(session && !session.invalid && session.expires > Date.now()), nonce: session?.nonce }
}
function badRequest(res: Response) {
res.status(400).send(JSON.stringify({ "error": "Invalid request body" }, null, 4));
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) {
res.status(401).send(JSON.stringify({ "error": "Unauthorized" }, 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) {

View file

@ -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.7",
"ulid": "^2.3.0",
"xlsx": "^0.17.3"
},

View file

@ -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';

View file

@ -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

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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';

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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;

View file

@ -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";

View file

@ -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";

View file

@ -1,5 +1,5 @@
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 ServerConfig from "../../../struct/ServerConfig";
import { getPermissionLevel } from "../../util";
@ -7,6 +7,7 @@ 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,
@ -17,6 +18,7 @@ type ServerDetails = {
bannerURL?: string,
serverConfig?: ServerConfig,
users: APIUser[],
channels: APIChannel[],
}
wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSResponse) => void) => {
@ -71,6 +73,13 @@ wsEvents.on('req:getUserServerDetails', async (data: ReqData, cb: (data: WSRespo
? { 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 });

View file

@ -1,4 +1,4 @@
import { User } from 'revolt.js/dist/maps/Users';
import { User } from '@janderedev/revolt.js/dist/maps/Users';
import { client } from '../../..';
import { getPermissionLevel, isBotManager } from '../../util';
import { wsEvents, WSResponse } from '../api_communication';

View file

@ -1,4 +1,4 @@
import { User } from "revolt.js/dist/maps/Users";
import { User } from "@janderedev/revolt.js/dist/maps/Users";
import { client } from "../../..";
import { getPermissionLevel, parseUser } from "../../util";
import { wsEvents, WSResponse } from "../api_communication";

View file

@ -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}`);

View file

@ -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) {

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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';

View file

@ -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";

View file

@ -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);
});
});
}
}

View file

@ -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";

View file

@ -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";

View file

@ -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 }

View file

@ -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";

View file

@ -1,4 +1,4 @@
import { ChannelPermission, ServerPermission } from "revolt.js";
import { ChannelPermission, ServerPermission } from "@janderedev/revolt.js";
class Command {
name: string;

View file

@ -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 {

View file

@ -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"
@ -504,23 +521,6 @@ revolt-api@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==
revolt.js@^5.2.7:
version "5.2.7"
resolved "https://registry.yarnpkg.com/revolt.js/-/revolt.js-5.2.7.tgz#7b887329913494a2caf02c9828685d63551890db"
integrity sha512-KNoQqLrdd/B8zryu2fhWim9rO5OEkouhCZj4nU+upwrekz30DjxqWgZCup/apKXE8PSmrhSgWdKT8SHCBXOxFQ==
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"
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"
@ -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"

View 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

View file

@ -1,19 +1,23 @@
import axios from 'axios';
import { FunctionComponent, useCallback, useEffect, useState } from "react";
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,
@ -24,6 +28,16 @@ type Server = {
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 = () => {
@ -37,6 +51,8 @@ const ServerDashboard: FunctionComponent = () => {
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 () => {
@ -71,13 +87,25 @@ const ServerDashboard: FunctionComponent = () => {
setBotManagers(server.serverConfig?.botManagers ?? []);
setModerators(server.serverConfig?.moderators ?? []);
loadAutomodInfo(server);
} catch(e: any) {
console.error(e);
setStatus(`${e?.message ?? e}`);
}
}, [serverInfo]);
useEffect(() => { loadInfo() }, []);
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 (
<>
@ -87,69 +115,115 @@ const ServerDashboard: FunctionComponent = () => {
<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>
<>
<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>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>
<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
@ -182,19 +256,13 @@ const ServerDashboard: FunctionComponent = () => {
display: 'inline-block',
}}
>{props.user.username ?? 'Unknown'}</span>
<div
style={{
marginLeft: '4px',
verticalAlign: 'middle',
display: 'inline-block',
height: '30px',
}}
<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);
}
@ -202,13 +270,7 @@ const ServerDashboard: FunctionComponent = () => {
setModerators(res.data.mods);
}
}}
>
<Icon // todo: hover effect
path={mdiCloseBox}
color='var(--tertiary-foreground)'
size='30px'
/>
</div>
/>
</div>
);
}
@ -248,7 +310,7 @@ const ServerDashboard: FunctionComponent = () => {
function UserListAddField(props: { type: 'MANAGER'|'MOD' }) {
const [content, setContent] = useState('');
const onConfirm = useCallback(async () => {
const onConfirm = useCallback(async () => {0
if (content.length) {
const res = await axios.put(
`${API_URL}/dash/server/${serverid}/${props.type == 'MANAGER' ? 'managers' : 'mods'}`,
@ -291,11 +353,217 @@ const ServerDashboard: FunctionComponent = () => {
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;