Merge branch 'master' of https://github.com/janderedev/automod
This commit is contained in:
commit
9b3eaa6752
14 changed files with 236 additions and 342 deletions
|
@ -13,7 +13,7 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@janderedev/revolt.js": "^6.0.0-rc.24-patch.1",
|
"@janderedev/revolt.js": "^6.0.0-patch.3",
|
||||||
"@types/monk": "^6.0.0",
|
"@types/monk": "^6.0.0",
|
||||||
"axios": "^0.22.0",
|
"axios": "^0.22.0",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
|
|
|
@ -5,11 +5,10 @@ import { commands, DEFAULT_PREFIX, ownerIDs } from "../../modules/command_handle
|
||||||
import child_process from 'child_process';
|
import child_process from 'child_process';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { wordlist } from "../../modules/user_scan";
|
|
||||||
import { User } from "@janderedev/revolt.js/dist/maps/Users";
|
import { User } from "@janderedev/revolt.js/dist/maps/Users";
|
||||||
import { adminBotLog } from "../../logging";
|
import { adminBotLog } from "../../logging";
|
||||||
import CommandCategory from "../../../struct/commands/CommandCategory";
|
import CommandCategory from "../../../struct/commands/CommandCategory";
|
||||||
import { parseUserOrId } from "../../util";
|
import { getMutualServers, parseUserOrId } from "../../util";
|
||||||
|
|
||||||
const BLACKLIST_BAN_REASON = `This user is globally blacklisted and has been banned automatically. If you wish to opt out of the global blacklist, run '/botctl ignore_blacklist yes'.`;
|
const BLACKLIST_BAN_REASON = `This user is globally blacklisted and has been banned automatically. If you wish to opt out of the global blacklist, run '/botctl ignore_blacklist yes'.`;
|
||||||
const BLACKLIST_MESSAGE = (username: string) => `\`@${username}\` has been banned automatically. Check the ban reason for more info.`;
|
const BLACKLIST_MESSAGE = (username: string) => `\`@${username}\` has been banned automatically. Check the ban reason for more info.`;
|
||||||
|
@ -76,8 +75,7 @@ export default {
|
||||||
+ `Heartbeat: \`${client.heartbeat}\`\n`
|
+ `Heartbeat: \`${client.heartbeat}\`\n`
|
||||||
+ `Ping: \`${client.websocket.ping ?? 'Unknown'}\`\n`
|
+ `Ping: \`${client.websocket.ping ?? 'Unknown'}\`\n`
|
||||||
+ `### Bot configuration\n`
|
+ `### Bot configuration\n`
|
||||||
+ `Owners: \`${ownerIDs.length}\` (${ownerIDs.join(', ')})\n`
|
+ `Owners: \`${ownerIDs.length}\` (${ownerIDs.join(', ')})\n`;
|
||||||
+ `Wordlist loaded: \`${wordlist ? `Yes (${wordlist.length} line${wordlist.length == 1 ? '' : 's'})` : 'No'}\`\n`;
|
|
||||||
|
|
||||||
await message.reply(msg, false);
|
await message.reply(msg, false);
|
||||||
break;
|
break;
|
||||||
|
@ -166,11 +164,8 @@ export default {
|
||||||
const msg = await message.reply(`User update stored.`);
|
const msg = await message.reply(`User update stored.`);
|
||||||
let bannedServers = 0;
|
let bannedServers = 0;
|
||||||
|
|
||||||
const mutuals = await target.fetchMutual();
|
const mutuals = getMutualServers(target);
|
||||||
for (const serverid of mutuals.servers) {
|
for (const server of mutuals) {
|
||||||
const server = client.servers.get(serverid);
|
|
||||||
if (!server) continue;
|
|
||||||
|
|
||||||
if (server.havePermission('BanMembers')) {
|
if (server.havePermission('BanMembers')) {
|
||||||
const config = await dbs.SERVERS.findOne({ id: server._id });
|
const config = await dbs.SERVERS.findOne({ id: server._id });
|
||||||
if (config?.allowBlacklistedUsers) continue;
|
if (config?.allowBlacklistedUsers) continue;
|
||||||
|
@ -188,7 +183,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(`Failed to ban in ${serverid}: ${e}`);
|
console.error(`Failed to ban in ${server._id}: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { FindOneResult } from "monk";
|
|
||||||
import { dbs } from "../../..";
|
import { dbs } from "../../..";
|
||||||
import CommandCategory from "../../../struct/commands/CommandCategory";
|
import CommandCategory from "../../../struct/commands/CommandCategory";
|
||||||
import SimpleCommand from "../../../struct/commands/SimpleCommand";
|
import SimpleCommand from "../../../struct/commands/SimpleCommand";
|
||||||
import MessageCommandContext from "../../../struct/MessageCommandContext";
|
import MessageCommandContext from "../../../struct/MessageCommandContext";
|
||||||
import ServerConfig from "../../../struct/ServerConfig";
|
|
||||||
import { scanServer } from "../../modules/user_scan";
|
|
||||||
import { isBotManager, NO_MANAGER_MSG } from "../../util";
|
import { isBotManager, NO_MANAGER_MSG } from "../../util";
|
||||||
|
|
||||||
let userscans: string[] = [];
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'botctl',
|
name: 'botctl',
|
||||||
aliases: null,
|
aliases: null,
|
||||||
|
@ -19,35 +14,6 @@ export default {
|
||||||
|
|
||||||
let action = args.shift();
|
let action = args.shift();
|
||||||
switch(action) {
|
switch(action) {
|
||||||
case 'scan_userlist':
|
|
||||||
try {
|
|
||||||
let serverConf: FindOneResult<ServerConfig> = await dbs.SERVERS.findOne({ id: message.serverContext._id });
|
|
||||||
|
|
||||||
if (!serverConf?.enableUserScan) return message.reply(`User scanning is not enabled for this server.`);
|
|
||||||
if (userscans.includes(message.serverContext._id)) return message.reply(`There is already a scan running for this server.`);
|
|
||||||
userscans.push(message.serverContext._id);
|
|
||||||
|
|
||||||
let msg = await message.reply(`Fetching users...`);
|
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
|
|
||||||
let onUserScan = async () => {
|
|
||||||
counter++;
|
|
||||||
if (counter % 10 == 0) await msg?.edit({ content: `Fetching users... ${counter}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
let onDone = async () => {
|
|
||||||
msg?.edit({ content: `All done! (${counter} users fetched)` });
|
|
||||||
userscans = userscans.filter(s => s != message.serverContext._id);
|
|
||||||
}
|
|
||||||
|
|
||||||
await scanServer(message.serverContext._id, onUserScan, onDone);
|
|
||||||
} catch(e) {
|
|
||||||
message.reply(`An error occurred: ${e}`);
|
|
||||||
userscans = userscans.filter(s => s != message.serverContext._id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ignore_blacklist':
|
case 'ignore_blacklist':
|
||||||
try {
|
try {
|
||||||
if (args[0] == 'yes') {
|
if (args[0] == 'yes') {
|
||||||
|
@ -68,7 +34,6 @@ export default {
|
||||||
case undefined:
|
case undefined:
|
||||||
case '':
|
case '':
|
||||||
message.reply(`### Available subcommands\n`
|
message.reply(`### Available subcommands\n`
|
||||||
+ `- \`scan_userlist\` - If user scanning is enabled, this will scan the entire user list.\n`
|
|
||||||
+ `- \`ignore_blacklist\` - Ignore the bot's global blacklist.`);
|
+ `- \`ignore_blacklist\` - Ignore the bot's global blacklist.`);
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -4,13 +4,14 @@ import Infraction from "../../../struct/antispam/Infraction";
|
||||||
import InfractionType from "../../../struct/antispam/InfractionType";
|
import InfractionType from "../../../struct/antispam/InfractionType";
|
||||||
import SimpleCommand from "../../../struct/commands/SimpleCommand";
|
import SimpleCommand from "../../../struct/commands/SimpleCommand";
|
||||||
import MessageCommandContext from "../../../struct/MessageCommandContext";
|
import MessageCommandContext from "../../../struct/MessageCommandContext";
|
||||||
import TempBan from "../../../struct/TempBan";
|
|
||||||
import { fetchUsername, logModAction } from "../../modules/mod_logs";
|
import { fetchUsername, logModAction } from "../../modules/mod_logs";
|
||||||
import { storeTempBan } from "../../modules/tempbans";
|
import { storeTempBan } from "../../modules/tempbans";
|
||||||
import { isModerator, NO_MANAGER_MSG, parseUserOrId, storeInfraction } from "../../util";
|
import { dedupeArray, embed, EmbedColor, isModerator, NO_MANAGER_MSG, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util";
|
||||||
import Day from 'dayjs';
|
import Day from 'dayjs';
|
||||||
import RelativeTime from 'dayjs/plugin/relativeTime';
|
import RelativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import CommandCategory from "../../../struct/commands/CommandCategory";
|
import CommandCategory from "../../../struct/commands/CommandCategory";
|
||||||
|
import { SendableEmbed } from "@janderedev/revolt.js/node_modules/revolt-api";
|
||||||
|
import { User } from "@janderedev/revolt.js";
|
||||||
|
|
||||||
Day.extend(RelativeTime);
|
Day.extend(RelativeTime);
|
||||||
|
|
||||||
|
@ -25,23 +26,20 @@ export default {
|
||||||
if (!await isModerator(message))
|
if (!await isModerator(message))
|
||||||
return message.reply(NO_MANAGER_MSG);
|
return message.reply(NO_MANAGER_MSG);
|
||||||
if (!message.serverContext.havePermission('BanMembers')) {
|
if (!message.serverContext.havePermission('BanMembers')) {
|
||||||
return await message.reply(`Sorry, I do not have \`BanMembers\` permission.`);
|
return await message.reply({ embeds: [
|
||||||
|
embed(`Sorry, I do not have \`BanMembers\` permission.`, '', EmbedColor.SoftError)
|
||||||
|
] });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.length == 0)
|
const userInput = args.shift() || '';
|
||||||
return message.reply(`You need to provide a target user!`);
|
if (!userInput && !message.reply_ids?.length) return message.reply({ embeds: [
|
||||||
|
embed(
|
||||||
const targetUser = await parseUserOrId(args.shift()!);
|
`Please specify one or more users by replying to their message while running this command or ` +
|
||||||
if (!targetUser) return message.reply('Sorry, I can\'t find that user.');
|
`by specifying a comma-separated list of usernames.`,
|
||||||
const targetName = await fetchUsername(targetUser._id);
|
'No target user specified',
|
||||||
|
EmbedColor.SoftError,
|
||||||
if (targetUser._id == message.author_id) {
|
),
|
||||||
return message.reply('nah');
|
] });
|
||||||
}
|
|
||||||
|
|
||||||
if (targetUser._id == client.user!._id) {
|
|
||||||
return message.reply('lol no');
|
|
||||||
}
|
|
||||||
|
|
||||||
let banDuration = 0;
|
let banDuration = 0;
|
||||||
let durationStr = args.shift();
|
let durationStr = args.shift();
|
||||||
|
@ -65,66 +63,190 @@ export default {
|
||||||
|
|
||||||
banDuration += num * multiplier;
|
banDuration += num * multiplier;
|
||||||
}
|
}
|
||||||
} else if (durationStr) args.splice(0, 0, durationStr);
|
} else if (durationStr) args.unshift(durationStr);
|
||||||
|
|
||||||
let reason = args.join(' ') || 'No reason provided';
|
let reason = args.join(' ')
|
||||||
|
?.replace(new RegExp('`', 'g'), '\'')
|
||||||
|
?.replace(new RegExp('\n', 'g'), ' ');
|
||||||
|
|
||||||
if (banDuration == 0) {
|
if (reason.length > 200) return message.reply({
|
||||||
let infId = ulid();
|
embeds: [ embed('Ban reason may not be longer than 200 characters.', null, EmbedColor.SoftError) ]
|
||||||
let infraction: Infraction = {
|
});
|
||||||
_id: infId,
|
|
||||||
createdBy: message.author_id,
|
const embeds: SendableEmbed[] = [];
|
||||||
date: Date.now(),
|
const handledUsers: string[] = [];
|
||||||
reason: reason,
|
const targetUsers: User|{ _id: string }[] = [];
|
||||||
server: message.serverContext._id,
|
|
||||||
type: InfractionType.Manual,
|
const targetInput = dedupeArray(
|
||||||
user: targetUser._id,
|
// Replied messages
|
||||||
actionType: 'ban',
|
(await Promise.allSettled(
|
||||||
|
(message.reply_ids ?? []).map(msg => message.channel?.fetchMessage(msg))
|
||||||
|
))
|
||||||
|
.filter(m => m.status == 'fulfilled').map(m => (m as any).value.author_id),
|
||||||
|
// Provided users
|
||||||
|
userInput.split(','),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const userStr of targetInput) {
|
||||||
|
try {
|
||||||
|
let user = await parseUserOrId(userStr);
|
||||||
|
if (!user) {
|
||||||
|
if (message.reply_ids?.length && userStr == userInput) {
|
||||||
|
reason = reason ? `${userInput} ${reason}` : userInput;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
embeds.push(embed(`I can't resolve \`${sanitizeMessageContent(userStr).trim()}\` to a user.`, null, '#ff785d'));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Silently ignore duplicates
|
||||||
|
if (handledUsers.includes(user._id)) continue;
|
||||||
|
handledUsers.push(user._id);
|
||||||
|
|
||||||
|
if (user._id == message.author_id) {
|
||||||
|
embeds.push(embed('I recommend against banning yourself :yeahokayyy:', null, EmbedColor.Warning));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user._id == client.user!._id) {
|
||||||
|
embeds.push(embed('I\'m not going to ban myself :flushee:', null, EmbedColor.Warning));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUsers.push(user);
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
embeds.push(embed(
|
||||||
|
`Failed to ban target \`${sanitizeMessageContent(userStr).trim()}\`: ${e}`,
|
||||||
|
`Failed to ban: An error has occurred`,
|
||||||
|
EmbedColor.Error,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let { userWarnCount } = await storeInfraction(infraction);
|
}
|
||||||
|
|
||||||
message.serverContext.banUser(targetUser._id, {
|
const members = await message.serverContext.fetchMembers();
|
||||||
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id})`
|
|
||||||
})
|
|
||||||
.catch(e => message.reply(`Failed to ban user: \`${e}\``));
|
|
||||||
|
|
||||||
await Promise.all([
|
for (const user of targetUsers) {
|
||||||
message.reply(`### ${targetName} has been ${Math.random() > 0.8 ? 'ejected' : 'banned'}.\n`
|
try {
|
||||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`),
|
if (banDuration == 0) {
|
||||||
logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction._id, `Ban duration: **Permanent**`),
|
const infId = ulid();
|
||||||
]);
|
const infraction: Infraction = {
|
||||||
} else {
|
_id: infId,
|
||||||
let banUntil = Date.now() + banDuration;
|
createdBy: message.author_id,
|
||||||
let infId = ulid();
|
date: Date.now(),
|
||||||
let infraction: Infraction = {
|
reason: reason || 'No reason provided',
|
||||||
_id: infId,
|
server: message.serverContext._id,
|
||||||
createdBy: message.author_id,
|
type: InfractionType.Manual,
|
||||||
date: Date.now(),
|
user: user._id,
|
||||||
reason: reason + ` (${durationStr})`,
|
actionType: 'ban',
|
||||||
server: message.serverContext._id,
|
}
|
||||||
type: InfractionType.Manual,
|
const { userWarnCount } = await storeInfraction(infraction);
|
||||||
user: targetUser._id,
|
|
||||||
actionType: 'ban',
|
const member = members.members.find(m => m._id.user == user._id);
|
||||||
|
|
||||||
|
if (member && message.member && !member.inferiorTo(message.member)) {
|
||||||
|
embeds.push(embed(
|
||||||
|
`\`${member.user?.username}\` has an equally or higher ranked role than you; refusing to ban.`,
|
||||||
|
'Missing permission',
|
||||||
|
EmbedColor.SoftError
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member && !member.bannable) {
|
||||||
|
embeds.push(embed(
|
||||||
|
`I don't have permission to ban \`${member?.user?.username || user._id}\`.`,
|
||||||
|
null,
|
||||||
|
EmbedColor.SoftError
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await message.serverContext.banUser(user._id, {
|
||||||
|
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id})`
|
||||||
|
});
|
||||||
|
|
||||||
|
await logModAction('ban', message.serverContext, message.member!, user._id, reason, infraction._id, `Ban duration: **Permanent**`);
|
||||||
|
|
||||||
|
embeds.push({
|
||||||
|
title: `User ${Math.random() > 0.8 ? 'ejected' : 'banned'}`,
|
||||||
|
icon_url: user instanceof User ? user.generateAvatarURL() : undefined,
|
||||||
|
colour: EmbedColor.Success,
|
||||||
|
description: `This is ${userWarnCount == 1 ? '**the first infraction**' : `infraction number **${userWarnCount}**`}` +
|
||||||
|
` for ${await fetchUsername(user._id)}.\n` +
|
||||||
|
`**Infraction ID:** \`${infraction._id}\`\n` +
|
||||||
|
`**Reason:** \`${infraction.reason}\``
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const banUntil = Date.now() + banDuration;
|
||||||
|
const banDurationFancy = Day(banUntil).fromNow(true);
|
||||||
|
const infId = ulid();
|
||||||
|
const infraction: Infraction = {
|
||||||
|
_id: infId,
|
||||||
|
createdBy: message.author_id,
|
||||||
|
date: Date.now(),
|
||||||
|
reason: (reason || 'No reason provided') + ` (${durationStr})`,
|
||||||
|
server: message.serverContext._id,
|
||||||
|
type: InfractionType.Manual,
|
||||||
|
user: user._id,
|
||||||
|
actionType: 'ban',
|
||||||
|
}
|
||||||
|
const { userWarnCount } = await storeInfraction(infraction);
|
||||||
|
|
||||||
|
await message.serverContext.banUser(user._id, {
|
||||||
|
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id}) (${durationStr})`
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
storeTempBan({
|
||||||
|
id: infId,
|
||||||
|
bannedUser: user._id,
|
||||||
|
server: message.serverContext._id,
|
||||||
|
until: banUntil,
|
||||||
|
}),
|
||||||
|
logModAction(
|
||||||
|
'ban',
|
||||||
|
message.serverContext,
|
||||||
|
message.member!,
|
||||||
|
user._id,
|
||||||
|
reason,
|
||||||
|
infraction._id,
|
||||||
|
`Ban duration: **${banDurationFancy}**`
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
embeds.push({
|
||||||
|
title: `User temporarily banned`,
|
||||||
|
icon_url: user instanceof User ? user.generateAvatarURL() : undefined,
|
||||||
|
colour: EmbedColor.Success,
|
||||||
|
description: `This is ${userWarnCount == 1 ? '**the first infraction**' : `infraction number **${userWarnCount}**`}` +
|
||||||
|
` for ${await fetchUsername(user._id)}.\n` +
|
||||||
|
`**Ban duration:** ` +
|
||||||
|
`**Infraction ID:** \`${infraction._id}\`\n` +
|
||||||
|
`**Reason:** \`${infraction.reason}\``
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
embeds.push(embed(
|
||||||
|
`Failed to ban target \`${await fetchUsername(user._id, user._id)}\`: ${e}`,
|
||||||
|
'Failed to ban: An error has occurred',
|
||||||
|
EmbedColor.Error,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let { userWarnCount } = await storeInfraction(infraction);
|
}
|
||||||
|
|
||||||
message.serverContext.banUser(targetUser._id, {
|
let firstMsg = true;
|
||||||
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id}) (${durationStr})`
|
while (embeds.length > 0) {
|
||||||
})
|
const targetEmbeds = embeds.splice(0, 10);
|
||||||
.catch(e => message.reply(`Failed to ban user: \`${e}\``));
|
|
||||||
|
|
||||||
await storeTempBan({
|
if (firstMsg) {
|
||||||
id: infId,
|
await message.reply({ embeds: targetEmbeds, content: 'Operation completed.' }, false);
|
||||||
bannedUser: targetUser._id,
|
} else {
|
||||||
server: message.serverContext._id,
|
await message.channel?.sendMessage({ embeds: targetEmbeds });
|
||||||
until: banUntil,
|
}
|
||||||
} as TempBan);
|
firstMsg = false;
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
message.reply(`### ${targetName} has been temporarily banned.\n`
|
|
||||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`),
|
|
||||||
logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction._id, `Ban duration: **${Day(banUntil).fromNow(true)}**`),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as SimpleCommand;
|
} as SimpleCommand;
|
||||||
|
|
|
@ -18,8 +18,8 @@ export default {
|
||||||
run: async (message: MessageCommandContext, args: string[]) => {
|
run: async (message: MessageCommandContext, args: string[]) => {
|
||||||
if (!await isModerator(message)) return message.reply(NO_MANAGER_MSG);
|
if (!await isModerator(message)) return message.reply(NO_MANAGER_MSG);
|
||||||
|
|
||||||
const userInput = args.shift();
|
const userInput = args.shift() || '';
|
||||||
if (!userInput) return message.reply({ embeds: [
|
if (!userInput && !message.reply_ids?.length) return message.reply({ embeds: [
|
||||||
embed(
|
embed(
|
||||||
`Please specify one or more users by replying to their message while running this command or ` +
|
`Please specify one or more users by replying to their message while running this command or ` +
|
||||||
`by specifying a comma-separated list of usernames.`,
|
`by specifying a comma-separated list of usernames.`,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { User } from '@janderedev/revolt.js/dist/maps/Users';
|
import { User } from '@janderedev/revolt.js/dist/maps/Users';
|
||||||
import { client } from '../../..';
|
import { client } from '../../..';
|
||||||
import { getPermissionLevel, isBotManager } from '../../util';
|
import { getMutualServers, getPermissionLevel } from '../../util';
|
||||||
import { wsEvents, WSResponse } from '../api_communication';
|
import { wsEvents, WSResponse } from '../api_communication';
|
||||||
|
|
||||||
type ReqData = { user: string }
|
type ReqData = { user: string }
|
||||||
|
@ -15,20 +15,19 @@ wsEvents.on('req:getUserServers', async (data: ReqData, cb: (data: WSResponse) =
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutuals = await user.fetchMutual();
|
const mutuals = getMutualServers(user);
|
||||||
|
|
||||||
type ServerResponse = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string }
|
type ServerResponse = { id: string, perms: 0|1|2|3, name: string, iconURL?: string, bannerURL?: string }
|
||||||
|
|
||||||
const promises: Promise<ServerResponse>[] = [];
|
const promises: Promise<ServerResponse>[] = [];
|
||||||
|
|
||||||
for (const sid of mutuals.servers) {
|
for (const server of mutuals) {
|
||||||
promises.push(new Promise(async (resolve, reject) => {
|
promises.push(new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const server = client.servers.get(sid);
|
|
||||||
if (!server) return reject('Server not found');
|
if (!server) return reject('Server not found');
|
||||||
const perms = await getPermissionLevel(user, server);
|
const perms = await getPermissionLevel(user, server);
|
||||||
resolve({
|
resolve({
|
||||||
id: sid,
|
id: server._id,
|
||||||
perms,
|
perms,
|
||||||
name: server.name,
|
name: server.name,
|
||||||
bannerURL: server.generateBannerURL(),
|
bannerURL: server.generateBannerURL(),
|
||||||
|
|
|
@ -138,6 +138,7 @@ let commands: SimpleCommand[];
|
||||||
try {
|
try {
|
||||||
await cmd.run(message, args);
|
await cmd.run(message, args);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
message.reply(`### An error has occurred:\n\`\`\`js\n${e}\n\`\`\``);
|
message.reply(`### An error has occurred:\n\`\`\`js\n${e}\n\`\`\``);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -149,11 +149,11 @@ async function logModAction(type: 'warn'|'kick'|'ban'|'votekick', server: Server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let fetchUsername = async (id: string) => {
|
let fetchUsername = async (id: string, fallbackText?: string) => {
|
||||||
try {
|
try {
|
||||||
let u = client.users.get(id) || await client.users.fetch(id);
|
let u = client.users.get(id) || await client.users.fetch(id);
|
||||||
return `@${u.username}`;
|
return `@${u.username}`;
|
||||||
} catch(e) { return 'Unknown user' }
|
} catch(e) { return fallbackText || 'Unknown user' }
|
||||||
}
|
}
|
||||||
|
|
||||||
export { fetchUsername, logModAction }
|
export { fetchUsername, logModAction }
|
||||||
|
|
|
@ -1,193 +0,0 @@
|
||||||
import { client, dbs } from "../..";
|
|
||||||
import fs from 'fs';
|
|
||||||
import { FindOneResult } from "monk";
|
|
||||||
import ScannedUser from "../../struct/ScannedUser";
|
|
||||||
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
|
|
||||||
import ServerConfig from "../../struct/ServerConfig";
|
|
||||||
import logger from "../logger";
|
|
||||||
import { sendLogMessage } from "../util";
|
|
||||||
|
|
||||||
let { USERSCAN_WORDLIST_PATH } = process.env;
|
|
||||||
|
|
||||||
let wordlist = USERSCAN_WORDLIST_PATH
|
|
||||||
? fs.readFileSync(USERSCAN_WORDLIST_PATH, 'utf8')
|
|
||||||
.split('\n')
|
|
||||||
.map(word => minifyText(word))
|
|
||||||
.filter(word => word.length > 0)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (wordlist) logger.info("Found word list; user scanning enabled");
|
|
||||||
|
|
||||||
let serverConfig: Map<string, ServerConfig> = new Map();
|
|
||||||
let userScanTimeout: Map<string, number> = new Map();
|
|
||||||
|
|
||||||
async function scanServer(id: string, userScanned: () => void, done: () => void) {
|
|
||||||
if (!wordlist) return;
|
|
||||||
let conf = await dbs.SERVERS.findOne({ id: id });
|
|
||||||
serverConfig.set(id, conf as ServerConfig);
|
|
||||||
if (!conf?.enableUserScan) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.debug(`Scanning user list for ${id}`);
|
|
||||||
|
|
||||||
let server = client.servers.get(id) || await client.servers.fetch(id);
|
|
||||||
let members = await server.fetchMembers(); // This can take multiple seconds, depending on the size of the server
|
|
||||||
|
|
||||||
for (const member of members.members) {
|
|
||||||
if (!member.user?.bot && member._id.user != client.user?._id) {
|
|
||||||
userScanned();
|
|
||||||
await scanUser(member);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
done();
|
|
||||||
} catch(e) { console.error(e) }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scanUser(member: Member) {
|
|
||||||
if (!wordlist) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let dbEntry: FindOneResult<ScannedUser|undefined>
|
|
||||||
= await dbs.SCANNED_USERS.findOne({ id: member._id.user, server: member.server?._id });
|
|
||||||
let user = member.user || await client.users.fetch(member._id.user);
|
|
||||||
let profile = await user.fetchProfile();
|
|
||||||
let report = false;
|
|
||||||
|
|
||||||
if (dbEntry) {
|
|
||||||
if (dbEntry.approved) return;
|
|
||||||
if (dbEntry.lastLog > Date.now() - (1000 * 60 * 60 * 48)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const word of wordlist) {
|
|
||||||
for (const text of [ user?.username, member.nickname, profile.content, user.status?.text ]) {
|
|
||||||
if (text && minifyText(text).includes(word)) report = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (report) {
|
|
||||||
if (dbEntry) {
|
|
||||||
await dbs.SCANNED_USERS.update({ _id: dbEntry._id }, {
|
|
||||||
$set: {
|
|
||||||
lastLog: Date.now(),
|
|
||||||
lastLoggedProfile: {
|
|
||||||
username: user.username,
|
|
||||||
nickname: member.nickname || undefined,
|
|
||||||
profile: profile.content || undefined,
|
|
||||||
status: user.status?.text || undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await dbs.SCANNED_USERS.insert({
|
|
||||||
approved: false,
|
|
||||||
id: user._id,
|
|
||||||
lastLog: Date.now(),
|
|
||||||
server: member.server!._id,
|
|
||||||
lastLoggedProfile: {
|
|
||||||
username: user.username,
|
|
||||||
nickname: member.nickname,
|
|
||||||
profile: profile.content,
|
|
||||||
status: user.status?.text,
|
|
||||||
}
|
|
||||||
} as ScannedUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
await logUser(member, profile);
|
|
||||||
}
|
|
||||||
} catch(e) { console.error(e) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async function logUser(member: Member, profile: any) { // `Profile` type doesn't seem to be exported by revolt.js
|
|
||||||
try {
|
|
||||||
let conf = serverConfig.get(member.server!._id);
|
|
||||||
if (!conf || !conf.enableUserScan) return;
|
|
||||||
|
|
||||||
logger.debug(`User ${member._id} matched word list; reporting`);
|
|
||||||
|
|
||||||
if (conf.enableUserScan && conf.logs?.userScan) {
|
|
||||||
let bannerUrl = client.generateFileURL({
|
|
||||||
_id: profile.background._id,
|
|
||||||
tag: profile.background.tag,
|
|
||||||
content_type: profile.background.content_type,
|
|
||||||
}, undefined, true);
|
|
||||||
let embedFields: { title: string, content: string, inline?: boolean }[] = [];
|
|
||||||
if (member.nickname) embedFields.push({ title: 'Nickname', content: member.nickname || 'None', inline: true });
|
|
||||||
if (member.user?.status?.text) embedFields.push({ title: 'Status', content: member.user.status.text || 'None', inline: true });
|
|
||||||
embedFields.push({ title: 'Profile', content: ((profile?.content || 'No about me text') as string).substring(0, 1000), inline: true });
|
|
||||||
|
|
||||||
sendLogMessage(conf.logs.userScan, {
|
|
||||||
title: 'Potentially suspicious user found',
|
|
||||||
description: `${member.user?.username ?? 'Unknown user'} | [${member._id.user}](/@${member._id.user}) | [Avatar](<${member.generateAvatarURL()}>)`,
|
|
||||||
color: '#ff9c11',
|
|
||||||
fields: embedFields,
|
|
||||||
image: bannerUrl ? {
|
|
||||||
type: 'BIG',
|
|
||||||
url: bannerUrl
|
|
||||||
} : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch(e) { console.error(e) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes symbols from a text to make it easier to match against the wordlist
|
|
||||||
function minifyText(text: string) {
|
|
||||||
return text
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s_./g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
new Promise((res: (value: void) => void) => client.user ? res() : client.once('ready', res)).then(() => {
|
|
||||||
client.on('packet', async packet => {
|
|
||||||
if (!wordlist) return;
|
|
||||||
if (packet.type == 'UserUpdate') {
|
|
||||||
try {
|
|
||||||
let user = client.users.get(packet.id);
|
|
||||||
if (!user || user.bot || user._id == client.user?._id) return;
|
|
||||||
let mutual = await user.fetchMutual();
|
|
||||||
|
|
||||||
mutual.servers.forEach(async sid => {
|
|
||||||
let server = client.servers.get(sid);
|
|
||||||
if (!server) return;
|
|
||||||
|
|
||||||
let conf = await dbs.SERVERS.findOne({ id: server._id });
|
|
||||||
serverConfig.set(server._id, conf as ServerConfig);
|
|
||||||
|
|
||||||
if (conf?.enableUserScan) {
|
|
||||||
let member = await server.fetchMember(packet.id);
|
|
||||||
let t = userScanTimeout.get(member._id.user);
|
|
||||||
if (t && t > (Date.now() - 10000)) return;
|
|
||||||
userScanTimeout.set(member._id.user, Date.now());
|
|
||||||
scanUser(member);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch(e) { console.error(e) }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('member/join', async (member) => {
|
|
||||||
if (!wordlist) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let user = member.user || await client.users.fetch(member._id.user);
|
|
||||||
if (!user || user.bot || user._id == client.user?._id) return;
|
|
||||||
|
|
||||||
let server = member.server || await client.servers.fetch(member._id.server);
|
|
||||||
if (!server) return;
|
|
||||||
|
|
||||||
let conf: FindOneResult<ServerConfig> = await dbs.SERVERS.findOne({ id: server._id });
|
|
||||||
serverConfig.set(server._id, conf as ServerConfig);
|
|
||||||
|
|
||||||
if (conf?.enableUserScan) {
|
|
||||||
let t = userScanTimeout.get(member._id.user);
|
|
||||||
if (t && t > (Date.now() - 10000)) return;
|
|
||||||
userScanTimeout.set(member._id.user, Date.now());
|
|
||||||
scanUser(member);
|
|
||||||
}
|
|
||||||
} catch(e) { console.error(e) }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export { scanServer, USERSCAN_WORDLIST_PATH, wordlist };
|
|
|
@ -346,6 +346,19 @@ function dedupeArray<T>(...arrays: T[][]): T[] {
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMutualServers(user: User) {
|
||||||
|
const servers: Server[] = [];
|
||||||
|
for (const member of client.members) {
|
||||||
|
if (member[1]._id.user == user._id && member[1].server) servers.push(member[1].server);
|
||||||
|
}
|
||||||
|
return servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const awaitClient = () => new Promise<void>(async resolve => {
|
||||||
|
if (!client.user) client.once('ready', () => resolve());
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getAutumnURL,
|
getAutumnURL,
|
||||||
hasPerm,
|
hasPerm,
|
||||||
|
@ -363,6 +376,8 @@ export {
|
||||||
sendLogMessage,
|
sendLogMessage,
|
||||||
embed,
|
embed,
|
||||||
dedupeArray,
|
dedupeArray,
|
||||||
|
awaitClient,
|
||||||
|
getMutualServers,
|
||||||
EmbedColor,
|
EmbedColor,
|
||||||
NO_MANAGER_MSG,
|
NO_MANAGER_MSG,
|
||||||
ULID_REGEX,
|
ULID_REGEX,
|
||||||
|
|
|
@ -10,7 +10,6 @@ import Infraction from './struct/antispam/Infraction';
|
||||||
import PendingLogin from './struct/PendingLogin';
|
import PendingLogin from './struct/PendingLogin';
|
||||||
import TempBan from './struct/TempBan';
|
import TempBan from './struct/TempBan';
|
||||||
import { VoteEntry } from './bot/commands/moderation/votekick';
|
import { VoteEntry } from './bot/commands/moderation/votekick';
|
||||||
import ScannedUser from './struct/ScannedUser';
|
|
||||||
import BridgeRequest from './struct/BridgeRequest';
|
import BridgeRequest from './struct/BridgeRequest';
|
||||||
import BridgeConfig from './struct/BridgeConfig';
|
import BridgeConfig from './struct/BridgeConfig';
|
||||||
import BridgedMessage from './struct/BridgedMessage';
|
import BridgedMessage from './struct/BridgedMessage';
|
||||||
|
@ -36,7 +35,6 @@ const dbs = {
|
||||||
SESSIONS: db.get('sessions'),
|
SESSIONS: db.get('sessions'),
|
||||||
TEMPBANS: db.get<TempBan>('tempbans'),
|
TEMPBANS: db.get<TempBan>('tempbans'),
|
||||||
VOTEKICKS: db.get<VoteEntry>('votekicks'),
|
VOTEKICKS: db.get<VoteEntry>('votekicks'),
|
||||||
SCANNED_USERS: db.get<ScannedUser>('scanned_users'),
|
|
||||||
BRIDGE_CONFIG: db.get<BridgeConfig>('bridge_config'),
|
BRIDGE_CONFIG: db.get<BridgeConfig>('bridge_config'),
|
||||||
BRIDGED_MESSAGES: db.get<BridgedMessage>('bridged_messages'),
|
BRIDGED_MESSAGES: db.get<BridgedMessage>('bridged_messages'),
|
||||||
BRIDGE_REQUESTS: db.get<BridgeRequest>('bridge_requests'),
|
BRIDGE_REQUESTS: db.get<BridgeRequest>('bridge_requests'),
|
||||||
|
@ -63,7 +61,6 @@ logger.info(`\
|
||||||
import('./bot/modules/mod_logs');
|
import('./bot/modules/mod_logs');
|
||||||
import('./bot/modules/event_handler');
|
import('./bot/modules/event_handler');
|
||||||
import('./bot/modules/tempbans');
|
import('./bot/modules/tempbans');
|
||||||
import('./bot/modules/user_scan');
|
|
||||||
import('./bot/modules/api_communication');
|
import('./bot/modules/api_communication');
|
||||||
import('./bot/modules/metrics');
|
import('./bot/modules/metrics');
|
||||||
import('./bot/modules/bot_status');
|
import('./bot/modules/bot_status');
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
class ScannedUser {
|
|
||||||
id: string;
|
|
||||||
server: string;
|
|
||||||
lastLog: number;
|
|
||||||
approved: boolean = false;
|
|
||||||
lastLoggedProfile?: {
|
|
||||||
username: string;
|
|
||||||
nickname?: string;
|
|
||||||
status?: string;
|
|
||||||
profile?: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScannedUser;
|
|
|
@ -23,9 +23,7 @@ class ServerConfig {
|
||||||
logs?: {
|
logs?: {
|
||||||
messageUpdate?: LogConfig, // Message edited or deleted
|
messageUpdate?: LogConfig, // Message edited or deleted
|
||||||
modAction?: LogConfig, // User warned, kicked or banned
|
modAction?: LogConfig, // User warned, kicked or banned
|
||||||
userScan?: LogConfig // User profile matched word list
|
|
||||||
};
|
};
|
||||||
enableUserScan?: boolean;
|
|
||||||
allowBlacklistedUsers?: boolean; // Whether the server explicitly allows users that are globally blacklisted
|
allowBlacklistedUsers?: boolean; // Whether the server explicitly allows users that are globally blacklisted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,10 +47,10 @@
|
||||||
axios "^0.26.1"
|
axios "^0.26.1"
|
||||||
openapi-typescript "^5.2.0"
|
openapi-typescript "^5.2.0"
|
||||||
|
|
||||||
"@janderedev/revolt.js@^6.0.0-rc.24-patch.1":
|
"@janderedev/revolt.js@^6.0.0-patch.3":
|
||||||
version "6.0.0-rc.24-patch.1"
|
version "6.0.0-patch.3"
|
||||||
resolved "https://registry.yarnpkg.com/@janderedev/revolt.js/-/revolt.js-6.0.0-rc.24-patch.1.tgz#58a9762fc887db16d34874d1e8a9c032112f6c83"
|
resolved "https://registry.yarnpkg.com/@janderedev/revolt.js/-/revolt.js-6.0.0-patch.3.tgz#7d9ad66e5a0d54fb2f5f0f8887cbd1382c69d301"
|
||||||
integrity sha512-+1Q94zNWj+OhRrBa4kVyFaG1U7OC2XwOrXqIk6Jiult8krH2lqKOjSqsxaKKITQLJ+8xIpM2TmMWVoXY52OdWQ==
|
integrity sha512-aZ1vubm8+l10lTy5HwO3vAc7E2/bm3+hfFhNqU7l+9QKecIAm6f45p7RNC19afSYChIZiyhtGPtWTuVJ9pa0RA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@insertish/exponential-backoff" "3.1.0-patch.2"
|
"@insertish/exponential-backoff" "3.1.0-patch.2"
|
||||||
"@insertish/isomorphic-ws" "^4.0.1"
|
"@insertish/isomorphic-ws" "^4.0.1"
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
lodash.isequal "^4.5.0"
|
lodash.isequal "^4.5.0"
|
||||||
long "^5.2.0"
|
long "^5.2.0"
|
||||||
mobx "^6.3.2"
|
mobx "^6.3.2"
|
||||||
revolt-api "0.5.3-rc.15"
|
revolt-api "0.5.3"
|
||||||
ulid "^2.3.0"
|
ulid "^2.3.0"
|
||||||
ws "^8.2.2"
|
ws "^8.2.2"
|
||||||
|
|
||||||
|
@ -600,7 +600,16 @@ require-at@^1.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a"
|
resolved "https://registry.yarnpkg.com/require-at/-/require-at-1.0.6.tgz#9eb7e3c5e00727f5a4744070a7f560d4de4f6e6a"
|
||||||
integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==
|
integrity sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==
|
||||||
|
|
||||||
revolt-api@0.5.3-rc.15, revolt-api@^0.5.3-rc.15:
|
revolt-api@0.5.3:
|
||||||
|
version "0.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3.tgz#e0ec2dcf812ea4338247b2eb77d67fc731d71b8a"
|
||||||
|
integrity sha512-hYdyStQiDZFvD+0dlf6SgQSiOk+JiEmQo0qIQHaqYRtrFN6FQBbGVNaiv7b5LzHHMPq7vks6ZVVA7hSNpcwlkA==
|
||||||
|
dependencies:
|
||||||
|
"@insertish/oapi" "0.1.15"
|
||||||
|
axios "^0.26.1"
|
||||||
|
lodash.defaultsdeep "^4.6.1"
|
||||||
|
|
||||||
|
revolt-api@^0.5.3-rc.15:
|
||||||
version "0.5.3-rc.15"
|
version "0.5.3-rc.15"
|
||||||
resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3-rc.15.tgz#abd08dd8109d0ca31be118461eabbeb6c3b7792e"
|
resolved "https://registry.yarnpkg.com/revolt-api/-/revolt-api-0.5.3-rc.15.tgz#abd08dd8109d0ca31be118461eabbeb6c3b7792e"
|
||||||
integrity sha512-MYin3U+KoObNkILPf2cz+FPperynExkUu7CjzurMJCRvBncpnhb2czvWDvnhLDKBHlpo8W597xNqzQnaklV4ug==
|
integrity sha512-MYin3U+KoObNkILPf2cz+FPperynExkUu7CjzurMJCRvBncpnhb2czvWDvnhLDKBHlpo8W597xNqzQnaklV4ug==
|
||||||
|
|
Loading…
Reference in a new issue