This commit is contained in:
JandereDev 2021-10-10 15:33:21 +02:00
parent 3e1da96360
commit 0d198c269b
Signed by: Lea
GPG key ID: 5D5E18ACB990F57A
16 changed files with 383 additions and 11 deletions

View file

@ -13,10 +13,12 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/monk": "^6.0.0", "@types/monk": "^6.0.0",
"axios": "^0.22.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"log75": "2.0.1", "log75": "2.0.1",
"monk": "^7.3.4", "monk": "^7.3.4",
"revolt.js": "^5.1.0-alpha.6" "revolt.js": "^5.1.0-alpha.6",
"ulid": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^4.4.3" "typescript": "^4.4.3"

View file

@ -0,0 +1,66 @@
import Command from "../../struct/Command";
import { Message } from "revolt.js/dist/maps/Messages";
import { hasPerm, parseUser } from "../util";
import ServerConfig from "../../struct/ServerConfig";
import { client } from "../..";
import { User } from "revolt.js/dist/maps/Users";
const SYNTAX = '/admin add @user; /admin remove @user; /admin list';
export default {
name: 'admin',
aliases: [ 'admins', 'manager', 'managers' ],
description: 'Allow users to control the bot\'s configuration',
syntax: SYNTAX,
serverOnly: true,
run: async (message: Message, 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 admins = config.botManagers ?? [];
let user: User|null;
switch(args[0]?.toLowerCase()) {
case 'add':
case 'new':
if (!args[1]) return message.reply('No user specified.');
user = await parseUser(args[1]);
if (!user) return message.reply('I can\'t find that user.');
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 } });
message.reply(`✅ Added \`@${user.username}\` to bot admins.`);
break;
case 'remove':
case 'delete':
case 'rm':
case 'del':
if (!args[1]) return message.reply('No user specified.');
user = await parseUser(args[1]);
if (!user) return message.reply('I can\'t find that user.');
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 } });
message.reply(`✅ Removed \`@${user.username}\` from bot admins.`);
break;
case 'list':
case 'ls':
case 'show':
message.reply(`# Bot admins\n`
+ `Users with **ManageServer** permission can add or remove admins.\n\n`
+ `${admins.map(a => `* <@${a}>`).join('\n')}\n\n`
+ `${admins.length} user${admins.length == 1 ? '' : 's'}.`)
?.catch(e => message.reply(e));
break;
default:
message.reply(`Available subcommands: ${SYNTAX}`);
}
}
} as Command;

View file

@ -11,7 +11,5 @@ export default {
message.reply(`Server ID: ${message.channel?.server_id || 'None'}\n` message.reply(`Server ID: ${message.channel?.server_id || 'None'}\n`
+ `Channel ID: ${message.channel_id}\n` + `Channel ID: ${message.channel_id}\n`
+ `User ID: ${message.author_id}`); + `User ID: ${message.author_id}`);
console.log(hasPerm(message.member!, 'BanMembers'));
} }
} as Command; } as Command;

42
src/bot/commands/eval.ts Normal file
View file

@ -0,0 +1,42 @@
import Command from "../../struct/Command";
import { Message } from "revolt.js/dist/maps/Messages";
import { inspect } from 'util';
export default {
name: 'eval',
aliases: [ 'e' ],
description: 'Evaluate JS code',
restrict: 'BOTOWNER',
removeEmptyArgs: false,
serverOnly: false,
run: async (message: Message, args: string[]) => {
let cmd = `let { client } = require("../..");`
+ `let axios = require("axios").default;`
+ `let crypto = require("crypto");`
+ args.join(' ');
let m = await message.channel?.sendMessage(`Executing...`);
try {
let e = eval(cmd);
if (e instanceof Promise) {
await m?.edit({ content: `## **Promise**<pending>` });
e.then((res) => {
m?.edit({
content: `## **Promise**<resolved>\n\`\`\`js\n${`${inspect(res)}`.substr(0, 1960)}\n\`\`\``
});
})
.catch((res) => {
m?.edit({
content: `## **Promise**<rejected>\n\`\`\`js\n${`${inspect(res)}`.substr(0, 1960)}\n\`\`\``
});
});
} else {
message.channel?.sendMessage(`\`\`\`js\n${inspect(e).substr(0, 1980)}\n\`\`\``);
}
} catch(e) {
m?.edit({ content: `## Execution failed\n\`\`\`js\n${inspect(e).substr(0, 1960)}\n\`\`\`` });
}
}
} as Command;

View file

@ -3,7 +3,7 @@ import { Message } from "revolt.js/dist/maps/Messages";
import { client } from "../.."; import { client } from "../..";
import ServerConfig from "../../struct/ServerConfig"; import ServerConfig from "../../struct/ServerConfig";
import { DEFAULT_PREFIX } from "../modules/command_handler"; import { DEFAULT_PREFIX } from "../modules/command_handler";
import { hasPerm } from "../util"; import { hasPerm, isBotManager, NO_MANAGER_MSG } from "../util";
const SYNTAX = '/prefix set [new prefix]; /prefix get; prefix clear'; const SYNTAX = '/prefix set [new prefix]; /prefix get; prefix clear';
const MENTION_TEXT = 'You can also @mention me instead of using the prefix.'; const MENTION_TEXT = 'You can also @mention me instead of using the prefix.';
@ -16,9 +16,10 @@ export default {
serverOnly: true, serverOnly: true,
run: async (message: Message, args: string[]) => { run: async (message: Message, args: string[]) => {
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.channel?.server_id })) ?? {};
switch(args[0]?.toLowerCase()) { switch(args[0]?.toLowerCase()) {
case 'set': case 'set':
if (!hasPerm(message.member!, 'ManageServer')) return message.reply('You need ManageServer permission for this.'); if (!await isBotManager(message.member!)) return message.reply(NO_MANAGER_MSG);
args.shift(); args.shift();
if (args.length == 0) return message.reply('You need to specify a prefix.'); if (args.length == 0) return message.reply('You need to specify a prefix.');
@ -41,7 +42,7 @@ export default {
break; break;
case 'clear': case 'clear':
case 'reset': case 'reset':
if (!hasPerm(message.member!, 'ManageServer')) return message.reply('You need ManageServer permission for this.'); if (!await isBotManager(message.member!)) return message.reply(NO_MANAGER_MSG);
if (config.prefix != null) { if (config.prefix != null) {
await client.db.get('servers').update({ 'id': message.channel?.server_id }, { $set: { 'prefix': null } }); await client.db.get('servers').update({ 'id': message.channel?.server_id }, { $set: { 'prefix': null } });

View file

@ -0,0 +1,33 @@
import Command from "../../struct/Command";
import { Message } from "revolt.js/dist/maps/Messages";
import { client } from "../..";
import AutomodSettings from "../../struct/antispam/AutomodSettings";
import AntispamRule from "../../struct/antispam/AntispamRule";
import ModerationAction from "../../struct/antispam/ModerationAction";
import { isBotManager, NO_MANAGER_MSG } from "../util";
import { ulid } from 'ulid';
export default {
name: 'settings',
aliases: [ 'setting' ],
description: 'change antispam settings',
serverOnly: false,
run: async (message: Message, args: string[]) => {
if (!isBotManager(message.member!)) return message.reply(NO_MANAGER_MSG);
let settings = {
spam: [
{
id: ulid(),
max_msg: 5,
timeframe: 3,
action: ModerationAction.Delete,
channels: [ '01FHJD5D2PBRTEVPNFM1FRY85J' ],
} as AntispamRule
]
} as AutomodSettings;
client.db.get('servers')
.update({ id: message.channel?.server_id }, { $set: { automodSettings: settings } });
}
} as Command;

View file

@ -0,0 +1,46 @@
import Command from "../../struct/Command";
import { Message } from "revolt.js/dist/maps/Messages";
import { exec } from 'child_process';
export default {
name: 'shell',
aliases: [ 'exec', 'sh' ],
description: 'Run code in a shell',
restrict: 'BOTOWNER',
removeEmptyArgs: false,
serverOnly: false,
run: async (message: Message, args: string[]) => {
let cmd = args.join(' ');
let m = await message.channel?.sendMessage(`Executing...`);
try {
let editMsg = () => {
if (str != '' && str != oldStr) {
if (str.length > 2000) {
str = str.substr(str.length - 2000);
}
m?.edit({ content: str })
.catch(e => console.warn('Failed to edit message'));
}
}
let str = '', oldStr = '';
let e = exec(cmd);
let i = setInterval(editMsg, 1000);
e.stdout?.on('data', m => {
str += m;
});
e.on('exit', (code) => {
clearInterval(i);
str += `\n\n**Exit code:** ${code}`;
editMsg();
});
} catch(e) {
message.channel?.sendMessage(`${e}`);
}
}
} as Command;

View file

@ -0,0 +1,65 @@
import { Message } from "revolt.js/dist/maps/Messages";
import { client } from "../..";
import ModerationAction from "../../struct/antispam/ModerationAction";
import ServerConfig from "../../struct/ServerConfig";
import logger from "../logger";
let msgCountStore: Map<string, { users: any }> = new Map();
/**
*
* @param message
* @returns true if ok, false if spam rule triggered
*/
async function antispam(message: Message): Promise<boolean> {
let serverRules: ServerConfig = await client.db.get('servers').findOne({ id: message.channel?.server_id }) ?? {};
if (!serverRules.automodSettings) return true;
let ruleTriggered = false;
for (const rule of serverRules.automodSettings.spam) {
if (msgCountStore.get(rule.id) == null) {
msgCountStore.set(rule.id, { users: {} });
}
if (rule.channels?.indexOf(message.channel_id) == -1) break;
let store = msgCountStore.get(rule.id)!;
if (!store.users[message.channel_id]) store.users[message.channel_id] = {}
let userStore = store.users[message.channel_id];
if (!userStore.count) userStore.count = 1;
else userStore.count++;
setTimeout(() => userStore.count--, rule.timeframe * 1000);
if (userStore.count > rule.max_msg) {
logger.info(`Antispam rule triggered: ${rule.max_msg}/${rule.timeframe} -> ${ModerationAction[rule.action]}`);
ruleTriggered = true;
switch(rule.action) {
case ModerationAction.Delete:
message.delete()
.catch(() => logger.warn('Antispam: Failed to delete message') );
break;
case ModerationAction.Warn:
if (!userStore.warnTriggered) {
userStore.warnTriggered = true;
setTimeout(() => userStore.warnTriggered = false, 5000);
message.channel?.sendMessage(`<@${message.author_id}>, stop spamming (placeholder warn message)`);
}
break;
case ModerationAction.Kick:
message.reply('(Kick user)');
break;
case ModerationAction.Ban:
message.reply('(Ban user)');
break;
}
}
}
return !ruleTriggered;
}
export { antispam }

View file

@ -4,6 +4,7 @@ import { client } from "../../index";
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import ServerConfig from "../../struct/ServerConfig"; import ServerConfig from "../../struct/ServerConfig";
import { antispam } from "./antispam";
const DEFAULT_PREFIX = process.env['PREFIX'] ?? '/'; const DEFAULT_PREFIX = process.env['PREFIX'] ?? '/';
@ -15,6 +16,9 @@ client.on('message', async message => {
logger.debug(`Message -> ${message.content}`); logger.debug(`Message -> ${message.content}`);
if (typeof message.content != 'string' || message.author_id == client.user?._id || !message.channel) return; if (typeof message.content != 'string' || message.author_id == client.user?._id || !message.channel) return;
// Send message through anti spam check
if (!antispam(message)) return;
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.channel?.server_id })) ?? {};
let guildPrefix = config.prefix ?? DEFAULT_PREFIX; let guildPrefix = config.prefix ?? DEFAULT_PREFIX;
@ -34,12 +38,26 @@ client.on('message', async message => {
let cmd = commands.find(c => c.name == cmdName || (c.aliases?.indexOf(cmdName!) ?? -1) > -1); let cmd = commands.find(c => c.name == cmdName || (c.aliases?.indexOf(cmdName!) ?? -1) > -1);
if (!cmd) return; 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');
return;
}
logger.info(`Command: ${message.author?.username} in ${message.channel?.server?.name}: ${message.content}`); logger.info(`Command: ${message.author?.username} in ${message.channel?.server?.name}: ${message.content}`);
// Create document for server in DB, if not already present
if (JSON.stringify(config) == '{}') await client.db.get('servers').insert({ id: message.channel?.server_id });
if (cmd.serverOnly && !message.channel?.server) { if (cmd.serverOnly && !message.channel?.server) {
return message.reply('This command is not available in direct messages.'); return message.reply('This command is not available in direct messages.');
} }
if (cmd.removeEmptyArgs !== false) {
args = args.filter(a => a.length > 0);
}
try { try {
cmd.run(message, args); cmd.run(message, args);
} catch(e) { } catch(e) {

View file

@ -1,4 +1,7 @@
import { Member } from "revolt.js/dist/maps/Members"; import { Member } from "revolt.js/dist/maps/Members";
import { User } from "revolt.js/dist/maps/Users";
import { client } from "..";
import ServerConfig from "../struct/ServerConfig";
let ServerPermissions = { let ServerPermissions = {
['View' as string]: 1 << 0, ['View' as string]: 1 << 0,
@ -13,15 +16,66 @@ let ServerPermissions = {
['RemoveAvatars' as string]: 1 << 15, ['RemoveAvatars' as string]: 1 << 15,
} }
const NO_MANAGER_MSG = '🔒 Missing permission';
const USER_MENTION_REGEX = /^<@[0-9A-HJ-KM-NP-TV-Z]{26}>$/i;
const CHANNEL_MENTION_REGEX = /^<#[0-9A-HJ-KM-NP-TV-Z]{26}>$/i;
/**
* Parses user input and returns an user object.
* Supports: `userID`, `<@userID>` (mention), `username`, `@username` (if user is cached).
* @param text
* @returns null if not found, otherwise user object
*/
async function parseUser(text: string): Promise<User|null> {
if (!text) return null;
let uid: string|null = null;
if (USER_MENTION_REGEX.test(text)) {
uid = text.replace(/<@|>/g, '').toUpperCase();
} else if (/^[0-9A-HJ-KM-NP-TV-Z]{26}$/gi.test(text)) {
uid = text.toUpperCase();
} else {
if (text.startsWith('@')) text = text.substr(1);
// Why is there no .find() or .filter()
let user: User|null = null;
client.users.forEach(u => {
if (u.username?.toLowerCase() == text.toLowerCase()) {
user = u;
}
});
if (user) return user;
}
if (uid) return await client.users.fetch(uid) || null;
else return null;
}
async function isBotManager(member: Member) {
return hasPerm(member, 'ManageServer')
|| (((await client.db.get('servers').findOne({ id: member.server?._id }) || {}) as ServerConfig)
.botManagers?.indexOf(member.user?._id!) ?? -1) > -1;
}
function hasPerm(member: Member, perm: 'View'|'ManageRoles'|'ManageChannels'|'ManageServer'| // its late and im tired function hasPerm(member: Member, perm: 'View'|'ManageRoles'|'ManageChannels'|'ManageServer'| // its late and im tired
'KickMembers'|'BanMembers'|'ChangeNickname'| // dont judge my code 'KickMembers'|'BanMembers'|'ChangeNickname'| // dont judge my code
'ManageNicknames'|'ChangeAvatar'|'RemoveAvatars') { 'ManageNicknames'|'ChangeAvatar'|'RemoveAvatars'): boolean {
let p = ServerPermissions[perm]; let p = ServerPermissions[perm];
if (member.server?.owner == member.user?._id) return true; if (member.server?.owner == member.user?._id) return true;
// TODO how the fuck do bitfields work // this should work but im not 100% certain
return false; let userPerm = member.roles?.map(id => member.server?.roles?.[id])
.reduce((sum: number, cur: any) => sum | cur.permissions[0], member.server?.default_permissions[0]) ?? 0;
return !!(userPerm & p);
} }
export { hasPerm } export {
hasPerm,
isBotManager,
parseUser,
NO_MANAGER_MSG,
USER_MENTION_REGEX,
CHANNEL_MENTION_REGEX
}

View file

@ -3,6 +3,8 @@ class Command {
aliases: string[] | null; aliases: string[] | null;
description: string | null; description: string | null;
syntax?: string | null; syntax?: string | null;
restrict?: 'BOTOWNER' | null;
removeEmptyArgs?: boolean | null;
run: Function; run: Function;
serverOnly: boolean; serverOnly: boolean;
} }

View file

@ -1,7 +1,11 @@
import AutomodSettings from "./antispam/AutomodSettings";
class ServerConfig { class ServerConfig {
id: string | undefined; id: string | undefined;
prefix: string | undefined; prefix: string | undefined;
spaceAfterPrefix: boolean | undefined; spaceAfterPrefix: boolean | undefined;
automodSettings: AutomodSettings | undefined;
botManagers: string[] | undefined;
} }
export default ServerConfig; export default ServerConfig;

View file

@ -0,0 +1,19 @@
import ModerationAction from "./ModerationAction";
/**
* Allow a maximum of X messages per X seconds.
* Example: max_msg = 5, timeframe = 3, action: Delete
* Allows a maximum of 5 messages within 3 seconds,
* and will delete any additional messages.
*
* `channels` optionally limits the rule to specific channels.
*/
class AntispamRule {
id: string;
max_msg: number;
timeframe: number;
action: ModerationAction;
channels: string[] | null;
}
export default AntispamRule;

View file

@ -0,0 +1,7 @@
import AntispamRule from "./AntispamRule";
class AutomodSettings {
spam: AntispamRule[];
}
export default AutomodSettings;

View file

@ -0,0 +1,8 @@
enum ModerationAction {
Delete = 0,
Warn = 1,
Kick = 2,
Ban = 3,
}
export default ModerationAction;

View file

@ -41,6 +41,13 @@ axios@^0.21.4:
dependencies: dependencies:
follow-redirects "^1.14.0" follow-redirects "^1.14.0"
axios@^0.22.0:
version "0.22.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.22.0.tgz#bf702c41fb50fbca4539589d839a077117b79b25"
integrity sha512-Z0U3uhqQeg1oNcihswf4ZD57O3NrR1+ZXhxaROaWpDmsDTx7T2HNBV2ulBtie2hwJptu8UvgnJoK+BIqdzh/1w==
dependencies:
follow-redirects "^1.14.4"
base64-js@^1.3.1: base64-js@^1.3.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -106,7 +113,7 @@ exponential-backoff@^3.1.0:
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.0.tgz#9409c7e579131f8bd4b32d7d8094a911040f2e68" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.0.tgz#9409c7e579131f8bd4b32d7d8094a911040f2e68"
integrity sha512-oBuz5SYz5zzyuHINoe9ooePwSu0xApKWgeNzok4hZ5YKXFh9zrQBEM15CXqoZkJJPuI2ArvqjPQd8UKJA753XA== integrity sha512-oBuz5SYz5zzyuHINoe9ooePwSu0xApKWgeNzok4hZ5YKXFh9zrQBEM15CXqoZkJJPuI2ArvqjPQd8UKJA753XA==
follow-redirects@^1.14.0: follow-redirects@^1.14.0, follow-redirects@^1.14.4:
version "1.14.4" version "1.14.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==