Allow linking servers together for moderation

This commit is contained in:
JandereDev 2021-12-05 15:18:12 +01:00
parent 1ec1b927fb
commit 36086994c8
Signed by: Lea
GPG key ID: 5D5E18ACB990F57A
15 changed files with 101 additions and 58 deletions

View file

@ -4,6 +4,7 @@ import { hasPerm, parseUser } from "../util";
import ServerConfig from "../../struct/ServerConfig";
import { client } from "../..";
import { User } from "@janderedev/revolt.js/dist/maps/Users";
import MessageCommandContext from "../../struct/MessageCommandContext";
const SYNTAX = '/admin add @user; /admin remove @user; /admin list';
@ -12,11 +13,11 @@ export default {
aliases: [ 'admins', 'manager', 'managers' ],
description: 'Allow users to control the bot\'s configuration',
syntax: SYNTAX,
run: async (message: Message, args: string[]) => {
run: async (message: MessageCommandContext, args: string[]) => {
if (!hasPerm(message.member!, 'ManageServer'))
return message.reply('You need **ManageServer** permission to use this command.');
let config: ServerConfig = (await client.db.get('servers').findOne({ id: message.channel?.server_id })) ?? {};
let config: ServerConfig = (await client.db.get('servers').findOne({ id: message.serverContext._id })) ?? {};
let admins = config.botManagers ?? [];
let user: User|null;
@ -30,7 +31,7 @@ export default {
if (admins.indexOf(user._id) > -1) return message.reply('This user is already added as bot admin.');
admins.push(user._id);
await client.db.get('servers').update({ id: message.channel?.server_id }, { $set: { botManagers: admins } });
await client.db.get('servers').update({ id: message.serverContext._id }, { $set: { botManagers: admins } });
message.reply(`✅ Added \`@${user.username}\` to bot admins.`);
break;
@ -45,7 +46,7 @@ export default {
if (admins.indexOf(user._id) == -1) return message.reply('This user is not added as bot admin.');
admins = admins.filter(a => a != user?._id);
await client.db.get('servers').update({ id: message.channel?.server_id }, { $set: { botManagers: admins } });
await client.db.get('servers').update({ id: message.serverContext._id }, { $set: { botManagers: admins } });
message.reply(`✅ Removed \`@${user.username}\` from bot admins.`);
break;

View file

@ -1,12 +1,14 @@
import Command from "../../struct/Command";
import { Message } from "@janderedev/revolt.js/dist/maps/Messages";
import MessageCommandContext from "../../struct/MessageCommandContext";
export default {
name: 'debug',
aliases: null,
description: 'give info helpful for development and debugging',
run: (message: Message, args: string[]) => {
run: (message: MessageCommandContext, args: string[]) => {
message.reply(`Server ID: ${message.channel?.server_id || 'None'}\n`
+ `Server context: ${message.serverContext._id} `
+ `(${message.serverContext._id == message.channel?.server_id ? ' This server' : message.serverContext.name})\n`
+ `Channel ID: ${message.channel_id}\n`
+ `User ID: ${message.author_id}`);
}

View file

@ -4,6 +4,7 @@ import { isBotManager, NO_MANAGER_MSG, parseUser } from "../util";
import ServerConfig from "../../struct/ServerConfig";
import { client } from "../..";
import { User } from "@janderedev/revolt.js/dist/maps/Users";
import MessageCommandContext from "../../struct/MessageCommandContext";
const SYNTAX = '/mod add @user; /mod remove @user; /mod list';
@ -14,10 +15,10 @@ export default {
aliases: [ 'moderators', 'mod', 'mods' ],
description: 'Allow users to moderate other users',
syntax: SYNTAX,
run: async (message: Message, args: string[]) => {
if (!await isBotManager(message.member!)) return message.reply(NO_MANAGER_MSG);
run: async (message: MessageCommandContext, args: string[]) => {
if (!await isBotManager(message.member!, message.channel?.server!)) return message.reply(NO_MANAGER_MSG);
let config: ServerConfig = (await client.db.get('servers').findOne({ id: message.channel?.server_id })) ?? {};
let config: ServerConfig = (await client.db.get('servers').findOne({ id: message.serverContext._id })) ?? {};
let mods = config.moderators ?? [];
let user: User|null;
@ -31,7 +32,7 @@ export default {
if (mods.indexOf(user._id) > -1) return message.reply('This user is already added as moderator.');
mods.push(user._id);
await client.db.get('servers').update({ id: message.channel?.server_id }, { $set: { moderators: mods } });
await client.db.get('servers').update({ id: message.serverContext._id }, { $set: { moderators: mods } });
message.reply(`✅ Added \`@${user.username}\` to moderators.`);
break;
@ -46,7 +47,7 @@ export default {
if (mods.indexOf(user._id) == -1) return message.reply('This user is not added as moderator.');
mods = mods.filter(a => a != user?._id);
await client.db.get('servers').update({ id: message.channel?.server_id }, { $set: { moderators: mods } });
await client.db.get('servers').update({ id: message.serverContext._id }, { $set: { moderators: mods } });
message.reply(`✅ Removed \`@${user.username}\` from moderators.`);
break;

View file

@ -18,7 +18,7 @@ export default {
switch(args[0]?.toLowerCase()) {
case 'set':
if (!await isBotManager(message.member!)) return message.reply(NO_MANAGER_MSG);
if (!await isBotManager(message.member!, message.channel?.server!)) return message.reply(NO_MANAGER_MSG);
args.shift();
if (args.length == 0) return message.reply('You need to specify a prefix.');
@ -41,7 +41,7 @@ export default {
break;
case 'clear':
case 'reset':
if (!await isBotManager(message.member!)) return message.reply(NO_MANAGER_MSG);
if (!await isBotManager(message.member!, message.channel?.server!)) return message.reply(NO_MANAGER_MSG);
if (config.prefix != null) {
await client.db.get('servers').update({ 'id': message.channel?.server_id }, { $set: { 'prefix': null } });

View file

@ -13,7 +13,7 @@ export default {
syntax: SYNTAX,
run: async (message: Message, args: string[]) => {
try {
if (!message.member || !await isModerator(message.member!)) return message.reply('🔒 Access denied');
if (!message.member || !await isModerator(message.member!, message.channel?.server!)) return message.reply('🔒 Access denied');
let messages: Array<Message> = [];
// X amount of messages from bottom

View file

@ -6,13 +6,16 @@ import AntispamRule from "../../struct/antispam/AntispamRule";
import ModerationAction from "../../struct/antispam/ModerationAction";
import { isBotManager, NO_MANAGER_MSG } from "../util";
import { ulid } from 'ulid';
import MessageCommandContext from "../../struct/MessageCommandContext";
export default {
name: 'settings',
aliases: [ 'setting' ],
description: 'change antispam settings',
run: async (message: Message, args: string[]) => {
if (!isBotManager(message.member!)) return message.reply(NO_MANAGER_MSG);
run: async (message: MessageCommandContext, args: string[]) => {
if (!isBotManager(message.member!, message.serverContext)) return message.reply(NO_MANAGER_MSG);
return 'command is disabled for now';
let settings = {
spam: [

View file

@ -5,14 +5,15 @@ import Infraction from "../../struct/antispam/Infraction";
import { ulid } from "ulid";
import InfractionType from "../../struct/antispam/InfractionType";
import { logModAction } from "../modules/mod_logs";
import MessageCommandContext from "../../struct/MessageCommandContext";
export default {
name: 'warn',
aliases: null,
removeEmptyArgs: false,
description: 'add an infraction to an user\'s record',
run: async (message: Message, args: string[]) => {
if (!await isModerator(message.member!)) return message.reply(NO_MANAGER_MSG);
run: async (message: MessageCommandContext, args: string[]) => {
if (!await isModerator(message.member!, message.serverContext)) return message.reply(NO_MANAGER_MSG);
let user = await parseUser(args.shift() ?? '');
if (!user) return message.reply('I can\'t find that user.');
if (user.bot != null) return message.reply('You cannot warn bots.');
@ -28,7 +29,7 @@ export default {
createdBy: message.author_id,
user: user._id,
reason: reason,
server: message.channel?.server_id!,
server: message.serverContext._id,
type: InfractionType.Manual,
date: Date.now(),
} as Infraction;
@ -36,7 +37,8 @@ export default {
let { userWarnCount } = await storeInfraction(infraction);
await Promise.all([
message.reply(`### User warned.\n`
message.reply(`### User warned`
+ `${message.serverContext._id != message.channel?.server_id ? ` in **${message.serverContext.name}**` : ''}.\n`
+ `This is ${userWarnCount == 1 ? '**the first warn**' : `warn number **${userWarnCount}**`}`
+ ` for ${user.username ?? 'this user'}.\n`
+ `**Infraction ID:** \`${infraction._id}\`\n`

View file

@ -10,6 +10,7 @@ import Xlsx from 'xlsx';
import FormData from 'form-data';
import axios from "axios";
import { fetchUsername } from "../modules/mod_logs";
import MessageCommandContext from "../../struct/MessageCommandContext";
Day.extend(RelativeTime);
@ -18,12 +19,12 @@ export default {
aliases: [ 'warnings', 'infractions', 'infraction' ],
description: 'Show all user infractions',
syntax: '/warns; /warns @username ["export-csv"]; /warns rm [ID]',
run: async (message: Message, args: string[]) => {
if (!await isModerator(message.member!)) return message.reply(NO_MANAGER_MSG);
run: async (message: MessageCommandContext, args: string[]) => {
if (!await isModerator(message.member!, message.serverContext)) return message.reply(NO_MANAGER_MSG);
let collection = client.db.get('infractions');
let infractions: Array<Infraction> = await collection.find({
server: message.channel?.server_id,
server: message.serverContext._id,
});
let userInfractions: Map<string, Infraction[]> = new Map();
infractions.forEach(i => {
@ -33,7 +34,7 @@ export default {
if (!args[0]) {
// Show top most warned users
let msg = `## Most warned users in ${message.channel?.server?.name ?? 'this server'}\n\u200b\n`;
let msg = `## Most warned users in ${message.serverContext.name}\n\u200b\n`;
for (let inf of Array.from(userInfractions.values()).sort((a, b) => b.length - a.length).slice(0, 9)) {
inf = inf.sort((a, b) => b.date - a.date);
msg += `**${await fetchUsername(inf[0].user)}** (${inf[0].user}): **${inf.length}** infractions\n`;
@ -52,7 +53,7 @@ export default {
if (!id) return message.reply('No infraction ID provided.');
let inf: Infraction|null = await client.db.get('infractions').findOneAndDelete({
_id: { $eq: id.toUpperCase() },
server: message.channel?.server_id
server: message.serverContext._id
});
if (!inf) return message.reply('I can\'t find that ID.');

View file

@ -2,6 +2,7 @@ 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";
import ServerConfig from "../../struct/ServerConfig";
import { isBotManager, NO_MANAGER_MSG, parseUser } from "../util";
@ -12,11 +13,11 @@ export default {
aliases: [],
description: 'Allow users or roles to bypass moderation rules',
syntax: SYNTAX,
run: async (message: Message, args: string[]) => {
let config: ServerConfig = await client.db.get('servers').findOne({ id: message.channel?.server_id }) || {}
run: async (message: MessageCommandContext, args: string[]) => {
let config: ServerConfig = await client.db.get('servers').findOne({ id: message.serverContext._id }) || {}
if (!config.whitelist) config.whitelist = { users: [], roles: [], managers: true }
if (!isBotManager(message.member!)) return message.reply(NO_MANAGER_MSG);
if (!isBotManager(message.member!, message.serverContext)) return message.reply(NO_MANAGER_MSG);
let user: User|null, role: string|undefined;
switch(args[0]?.toLowerCase()) {
@ -24,7 +25,7 @@ export default {
case 'set':
if (!args[1]) return message.reply('You need to spefify a user or role name.');
role = Object.entries(message.channel?.server?.roles ?? {})
role = Object.entries(message.serverContext.roles ?? {})
.find((r) => r[1].name?.toLowerCase() == args[1].toLowerCase()
|| r[0] == args[1].toUpperCase())
?.[0];
@ -34,7 +35,7 @@ export default {
return message.reply('That role is already whitelisted.');
config.whitelist!.roles = [role, ...(config.whitelist!.roles ?? [])];
await client.db.get('servers').update({ id: message.channel?.server_id }, { $set: { whitelist: config.whitelist } });
await client.db.get('servers').update({ id: message.serverContext._id }, { $set: { whitelist: config.whitelist } });
return message.reply(`Added role to whitelist!`);
}
@ -45,7 +46,7 @@ export default {
return message.reply('That user is already whitelisted.');
config.whitelist!.users = [user._id, ...(config.whitelist!.users ?? [])];
await client.db.get('servers').update({ id: message.channel?.server_id }, { $set: { whitelist: config.whitelist } });
await client.db.get('servers').update({ id: message.serverContext._id }, { $set: { whitelist: config.whitelist } });
return message.reply('Added user to whitelist!');
break;
case 'rm':
@ -54,7 +55,7 @@ export default {
case 'delete':
if (!args[1]) return message.reply('You need to spefify a user or role name.');
role = Object.entries(message.channel?.server?.roles ?? {})
role = Object.entries(message.serverContext.roles ?? {})
.find((r) => r[1].name?.toLowerCase() == args[1].toLowerCase()
|| r[0] == args[1].toUpperCase())
?.[0];
@ -64,7 +65,7 @@ export default {
return message.reply('That role is not whitelisted.');
config.whitelist!.roles = config.whitelist!.roles.filter(r => r != role);
await client.db.get('servers').update({ id: message.channel?.server_id }, { $set: { whitelist: config.whitelist } });
await client.db.get('servers').update({ id: message.serverContext._id }, { $set: { whitelist: config.whitelist } });
return message.reply(`Removed role from whitelist!`);
}
@ -74,7 +75,7 @@ export default {
return message.reply('That user is not whitelisted.');
config.whitelist!.users = config.whitelist!.users.filter(u => u != user?._id);
await client.db.get('servers').update({ id: message.channel?.server_id }, { $set: { whitelist: config.whitelist } });
await client.db.get('servers').update({ id: message.serverContext._id }, { $set: { whitelist: config.whitelist } });
return message.reply('Removed user from whitelist!');
break;
case 'l':
@ -95,7 +96,7 @@ export default {
if (config.whitelist.roles?.length) {
config.whitelist.roles
?.map(r => message.channel?.server?.roles?.[r]?.name || `Unknown role (${r})`)
?.map(r => message.serverContext.roles?.[r]?.name || `Unknown role (${r})`)
.forEach((r, index) => {
if (index < 15) str += `* ${r}\n`;
if (index == 15) str += `**${config.whitelist!.roles!.length - 15} more role${config.whitelist?.roles?.length == 16 ? '' : 's'}**\n`;

View file

@ -7,7 +7,7 @@ import InfractionType from "../../struct/antispam/InfractionType";
import ModerationAction from "../../struct/antispam/ModerationAction";
import ServerConfig from "../../struct/ServerConfig";
import logger from "../logger";
import { isBotManager, isModerator, storeInfraction } from "../util";
import { isModerator, storeInfraction } from "../util";
let msgCountStore: Map<string, { users: any }> = new Map();
@ -30,7 +30,7 @@ async function antispam(message: Message): Promise<boolean> {
if (message.author?.bot != null) break;
if (serverRules.whitelist?.users?.includes(message.author_id)) break;
if (message.member?.roles?.filter(r => serverRules.whitelist?.roles?.includes(r)).length) break;
if (serverRules.whitelist?.managers !== false && await isModerator(message.member!)) break;
if (serverRules.whitelist?.managers !== false && await isModerator(message.member!, message.channel?.server!)) break;
if (rule.channels?.indexOf(message.channel_id) == -1) break;
let store = msgCountStore.get(rule.id)!;

View file

@ -6,6 +6,7 @@ import path from 'path';
import ServerConfig from "../../struct/ServerConfig";
import { antispam } from "./antispam";
import checkCustomRules from "./custom_rules/custom_rules";
import MessageCommandContext from "../../struct/MessageCommandContext";
const DEFAULT_PREFIX = process.env['PREFIX']
?? process.env['BOT_PREFIX']
@ -16,23 +17,23 @@ let commands: Command[] = fs.readdirSync(path.join(__dirname, '..', 'commands'))
.filter(file => file.endsWith('.js'))
.map(file => require(path.join(__dirname, '..', 'commands', file)).default as Command);
client.on('message', async message => {
logger.debug(`Message -> ${message.content}`);
client.on('message', async msg => {
logger.debug(`Message -> ${msg.content}`);
if (typeof message.content != 'string' ||
message.author_id == client.user?._id ||
!message.channel?.server) return;
if (typeof msg.content != 'string' ||
msg.author_id == client.user?._id ||
!msg.channel?.server) return;
// Send message through anti spam check and custom rules
if (!await antispam(message)) return;
checkCustomRules(message);
if (!await antispam(msg)) return;
checkCustomRules(msg);
let config: ServerConfig = (await client.db.get('servers').findOne({ 'id': message.channel?.server_id })) ?? {};
let guildPrefix = config.prefix ?? DEFAULT_PREFIX;
let args = message.content.split(' ');
let args = msg.content.split(' ');
let cmdName = args.shift() ?? '';
let config: ServerConfig = (await client.db.get('servers').findOne({ 'id': msg.channel?.server_id })) ?? {};
let guildPrefix = config.prefix ?? DEFAULT_PREFIX;
if (cmdName.startsWith(`<@${client.user?._id}>`)) {
cmdName = cmdName.substr(`<@${client.user?._id}>`.length);
if (!cmdName) cmdName = args.shift() ?? ''; // Space between mention and command name
@ -47,12 +48,28 @@ client.on('message', async message => {
if (!cmd) return;
let ownerIDs = process.env['BOT_OWNERS'] ? process.env['BOT_OWNERS'].split(',') : [];
if (cmd.restrict == 'BOTOWNER' && ownerIDs.indexOf(message.author_id) == -1) {
logger.warn(`User ${message.author?.username} tried to run owner-only command: ${cmdName}`);
message.reply('🔒 Access denied');
if (cmd.restrict == 'BOTOWNER' && ownerIDs.indexOf(msg.author_id) == -1) {
logger.warn(`User ${msg.author?.username} tried to run owner-only command: ${cmdName}`);
msg.reply('🔒 Access denied');
return;
}
let serverCtx = msg.channel?.server;
if (config.linkedServer) {
try {
serverCtx = client.servers.get(config.linkedServer)
|| await client.servers.fetch(config.linkedServer);
} catch(e) {
msg.reply(`# Error\n` +
`Failed to fetch linked server. This command will be executed in the context of this server.\n\n` +
`Error: \`\`\`js\n${e}\n\`\`\``);
}
}
let message: MessageCommandContext = msg as MessageCommandContext;
message.serverContext = serverCtx;
logger.info(`Command: ${message.author?.username} in ${message.channel?.server?.name}: ${message.content}`);
// Create document for server in DB, if not already present

View file

@ -5,6 +5,7 @@ import Infraction from "../struct/antispam/Infraction";
import ServerConfig from "../struct/ServerConfig";
import FormData from 'form-data';
import axios from 'axios';
import { Server } from "@janderedev/revolt.js/dist/maps/Servers";
let ServerPermissions = {
['View' as string]: 1 << 0,
@ -65,15 +66,15 @@ async function parseUser(text: string): Promise<User|null> {
} catch(e) { return null; }
}
async function isModerator(member: Member) {
async function isModerator(member: Member, server: Server) {
return hasPerm(member, 'KickMembers')
|| await isBotManager(member)
|| (((await client.db.get('servers').findOne({ id: member.server?._id }) || {}) as ServerConfig)
|| await isBotManager(member, server)
|| (((await client.db.get('servers').findOne({ id: server._id }) || {}) as ServerConfig)
.moderators?.indexOf(member.user?._id!) ?? -1) > -1;
}
async function isBotManager(member: Member) {
async function isBotManager(member: Member, server: Server) {
return hasPerm(member, 'ManageServer')
|| (((await client.db.get('servers').findOne({ id: member.server?._id }) || {}) as ServerConfig)
|| (((await client.db.get('servers').findOne({ id: server._id }) || {}) as ServerConfig)
.botManagers?.indexOf(member.user?._id!) ?? -1) > -1;
}

View file

@ -13,7 +13,7 @@ class AutomodClient extends Revolt.Client {
}
let login = (client: Revolt.Client): Promise<void> => new Promise((resolve, reject) => {
logger.info('Logging in...');
logger.info('Bot logging in...');
let env = process.env;
if (!env['BOT_TOKEN']) {
@ -24,7 +24,7 @@ let login = (client: Revolt.Client): Promise<void> => new Promise((resolve, reje
client.loginBot(env['BOT_TOKEN']);
client.once('ready', () => {
logger.done('Logged in!');
logger.done('Bot logged in!');
resolve();
});
});

View file

@ -0,0 +1,13 @@
import { Message } from "@janderedev/revolt.js/dist/maps/Messages";
import { Server } from "@janderedev/revolt.js/dist/maps/Servers";
class MessageCommandContext extends Message {
// The server to which the command should be applied.
serverContext: Server;
/* Override types */
content: string;
}
export default MessageCommandContext;

View file

@ -7,6 +7,7 @@ class ServerConfig {
automodSettings: AutomodSettings | undefined;
botManagers: string[] | undefined;
moderators: string[] | undefined;
linkedServer: string | undefined;
whitelist: {
users: string[] | undefined,
roles: string[] | undefined,