add antispam settings to dashboard, various fixes
also switched to revolt.js fork
This commit is contained in:
parent
dd145db89d
commit
860b816136
40 changed files with 526 additions and 172 deletions
|
@ -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');
|
||||
})();
|
||||
|
||||
|
|
|
@ -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', '*');
|
||||
|
|
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 });
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
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 |
|
@ -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,6 +115,7 @@ 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', }}
|
||||
|
@ -111,9 +140,11 @@ const ServerDashboard: FunctionComponent = () => {
|
|||
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
|
||||
|
@ -145,11 +176,54 @@ const ServerDashboard: FunctionComponent = () => {
|
|||
<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,13 +256,7 @@ 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}`,
|
||||
|
@ -202,14 +270,8 @@ 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;
|
||||
|
|
Loading…
Reference in a new issue