From 347c13c1444b3757c70b141c3e62d6a66abb7107 Mon Sep 17 00:00:00 2001 From: MonzterDEV Date: Tue, 12 Sep 2023 02:03:28 -0400 Subject: [PATCH] feat: added blocked messages to message filters --- config/schema/config.json | 15 ++ config/schema/system.json | 2 +- src/automod/MessageFilter.ts | 61 ++++-- .../settings/BlockedMessageCommand.ts | 207 ++++++++++++++++++ src/commands/settings/BlockedTokenCommand.ts | 3 +- src/commands/settings/BlockedWordCommand.ts | 3 +- src/services/LoggerService.ts | 31 ++- src/types/GuildConfigSchema.ts | 13 +- 8 files changed, 306 insertions(+), 29 deletions(-) create mode 100644 src/commands/settings/BlockedMessageCommand.ts diff --git a/config/schema/config.json b/config/schema/config.json index 3bbb01574..ebb9432b0 100755 --- a/config/schema/config.json +++ b/config/schema/config.json @@ -253,6 +253,10 @@ "blocked_tokens": { "type": "boolean", "default": false + }, + "blocked_messages": { + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -275,6 +279,10 @@ "blocked_tokens": { "type": "boolean", "default": false + }, + "blocked_messages": { + "type": "boolean", + "default": false } }, "additionalProperties": false @@ -298,6 +306,13 @@ "type": "string" }, "default": [] + }, + "blocked_messages": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] } }, "additionalProperties": false, diff --git a/config/schema/system.json b/config/schema/system.json index 96933e940..1ca7f52cf 100755 --- a/config/schema/system.json +++ b/config/schema/system.json @@ -151,7 +151,7 @@ "format": "date-time" } ], - "default": "2023-09-11T12:15:47.307Z" + "default": "2023-09-12T06:02:24.592Z" } }, "additionalProperties": false, diff --git a/src/automod/MessageFilter.ts b/src/automod/MessageFilter.ts index 6b4dfb70f..9b637f7bb 100644 --- a/src/automod/MessageFilter.ts +++ b/src/automod/MessageFilter.ts @@ -49,28 +49,36 @@ export default class MessageFilter extends Service implements HasEventListeners const blockedWords = config?.data?.blocked_words ?? []; const blockedTokens = config?.data?.blocked_tokens ?? []; + const blockedMessages = config?.data?.blocked_messages ?? []; const { safe: tokenSafe, token } = await this.filterTokens(message, blockedTokens); const { safe: wordSafe, word } = await this.filterWords(message, blockedWords); + const { safe: messageSafe, theMessage } = await this.filterMessages(message, blockedMessages); if ( - (!tokenSafe || !wordSafe) && - ((!tokenSafe && - (config.send_logs === true || (typeof config.send_logs === "object" && config.send_logs.blocked_tokens))) || + (!tokenSafe || !wordSafe || !messageSafe) && + ( + (!tokenSafe && + (config.send_logs === true || (typeof config.send_logs === "object" && config.send_logs.blocked_tokens))) || (!wordSafe && - (config.send_logs === true || (typeof config.send_logs === "object" && config.send_logs.blocked_words)))) - ) { - this.client.logger - .logBlockedWordOrToken({ - guild: message.guild!, - content: message.content, - isToken: !tokenSafe, - user: message.author, - token: !tokenSafe ? token : undefined, - word: !wordSafe ? word : undefined - }) - .catch(logError); - } + (config.send_logs === true || (typeof config.send_logs === "object" && config.send_logs.blocked_words))) || + (!messageSafe && + (config.send_logs === true || (typeof config.send_logs === "object" && config.send_logs.blocked_messages))) + ) + ) { + const blockType = !tokenSafe ? "token" : !wordSafe ? "word" : "message"; + this.client.logger + .logBlockedWordOrToken({ + guild: message.guild!, + content: message.content, + blockType: blockType, + user: message.author, + token: !tokenSafe ? token : undefined, + word: !wordSafe ? word : undefined, + message: !messageSafe ? theMessage : undefined, + }) + .catch(logError); + } if (!message.deletable) return false; @@ -91,6 +99,14 @@ export default class MessageFilter extends Service implements HasEventListeners return true; } + if ( + !messageSafe && + (config.delete_message === true || (typeof config.delete_message === "object" && config.delete_message.blocked_messages)) + ) { + message.delete().catch(logError); + return true; + } + return false; } @@ -115,4 +131,17 @@ export default class MessageFilter extends Service implements HasEventListeners return { safe: true }; } + + async filterMessages(message: Message, blockedMessages: string[]) { + const content = message.content.toLowerCase() + + for (const blockedMessage of blockedMessages) { + if (content === blockedMessage.toLowerCase()) { + return { safe: false, theMessage: blockedMessage }; + } + } + + return { safe: true }; + } + } diff --git a/src/commands/settings/BlockedMessageCommand.ts b/src/commands/settings/BlockedMessageCommand.ts new file mode 100644 index 000000000..cbab2f804 --- /dev/null +++ b/src/commands/settings/BlockedMessageCommand.ts @@ -0,0 +1,207 @@ +/** + * This file is part of SudoBot. + * + * Copyright (C) 2021-2023 OSN Developers. + * + * SudoBot is free software; you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SudoBot is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with SudoBot. If not, see . + */ + +import { EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder, Snowflake, escapeMarkdown } from "discord.js"; +import Command, { ArgumentType, BasicCommandContext, CommandMessage, CommandReturn, ValidationRule } from "../../core/Command"; +import Pagination from "../../utils/Pagination"; + +export default class BlockedMessageCommand extends Command { + public readonly subcommandsCustom = ["add", "remove", "has", "list"]; + public readonly name = "blockedmessage"; + public readonly validationRules: ValidationRule[] = [ + { + types: [ArgumentType.String], + requiredErrorMessage: `Please provide a subcommand! The valid subcommands are: \`${this.subcommandsCustom.join("`, `")}\`.`, + typeErrorMessage: `Please provide a __valid__ subcommand! The valid subcommands are: \`${this.subcommandsCustom.join("`, `")}\`.`, + name: "subcommand" + } + ]; + public readonly permissions = [PermissionFlagsBits.ManageGuild, PermissionFlagsBits.BanMembers]; + public readonly permissionMode = "or"; + + public readonly description = "Manage blocked messages."; + + public readonly detailedDescription = [ + "Add/remove/check/view the blocked messages. All arguments, separated by spaces will be treated as different messages.\n", + "**Subcommands**", + "* `add <...messages>` - Add blocked message(s)", + "* `remove <...messages>` - Remove blocked message(s)", + "* `has ` - Check if the given message is blocked", + "* `list` - List all the blocked messages" + ].join("\n"); + + public readonly argumentSyntaxes = [" [...args]"]; + + public readonly slashCommandBuilder = new SlashCommandBuilder() + .addSubcommand(subcommand => + subcommand + .setName("add") + .setDescription("Add a blocked message") + .addStringOption(option => option.setName("message").setDescription("The message to block").setRequired(true)) + ) + .addSubcommand(subcommand => + subcommand + .setName("remove") + .setDescription("Remove blocked message") + .addStringOption(option => option.setName("message").setDescription("The message to remove from blocklist").setRequired(true)) + ) + .addSubcommand(subcommand => + subcommand + .setName("has") + .setDescription("Check if a blocked message exists in the blocklist") + .addStringOption(option => option.setName("message").setDescription("The message to check").setRequired(true)) + ) + .addSubcommand(subcommand => subcommand.setName("list").setDescription("Show the blocked message list")); + public readonly aliases = ["blockedmessages"]; + + createConfigIfNotExists(guildId: Snowflake) { + this.client.configManager.config[guildId!]!.message_filter ??= { + enabled: true, + delete_message: true, + send_logs: true + } as any; + + this.client.configManager.config[guildId!]!.message_filter!.data ??= { + blocked_tokens: [], + blocked_words: [], + blocked_messages: [] + }; + + this.client.configManager.config[guildId!]!.message_filter!.data!.blocked_messages ??= []; + } + + async execute(message: CommandMessage, context: BasicCommandContext): Promise { + const subcommand = (context.isLegacy ? context.parsedNamedArgs.subcommand : context.options.getSubcommand(true))?.toString(); + + if (!this.subcommandsCustom.includes(subcommand)) { + await this.error(message, `Invalid subcommand provided. The valid subcommands are: \`${this.subcommandsCustom.join("`, `")}\`.`); + return; + } + + if (context.isLegacy && context.args[1] === undefined && subcommand !== "list") { + await this.error( + message, + `You must specify a message ${subcommand === "add" ? "to block" : subcommand === "remove" ? "to remove" : "to check"}!` + ); + return; + } + + if (!this.client.configManager.config[message.guildId!]) { + return; + } + + await this.deferIfInteraction(message); + + if (context.isLegacy) { + context.args.shift(); + } + + this.createConfigIfNotExists(message.guildId!); + + switch (subcommand) { + case "add": + const messageToBlock = context.isLegacy ? context.args[0] : context.options.getString("message", true) + + if (!this.client.configManager.config[message.guildId!]?.message_filter?.data?.blocked_messages.includes(messageToBlock)) { + this.client.configManager.config[message.guildId!]?.message_filter?.data?.blocked_messages.push(messageToBlock); + } + + await this.client.configManager.write(); + await this.success(message, `The given message has been blocked.`); + break; + + case "has": + const messageToCheck = context.isLegacy ? context.args[0] : context.options.getString("message", true); + + if (this.client.configManager.config[message.guildId!]?.message_filter?.data?.blocked_messages.includes(messageToCheck)) { + await this.success(message, `This message is in the blocklist.`); + } else { + await this.error(message, `This message is not in the blocklist.`); + } + + return; + + case "remove": + const messageToRemove = context.isLegacy ? context.args[0] : context.options.getString("message", true) + + const index = this.client.configManager.config[message.guildId!]?.message_filter?.data?.blocked_messages.indexOf(messageToRemove); + + if (!index || index === -1) { + return; + } + + this.client.configManager.config[message.guildId!]?.message_filter?.data?.blocked_messages.splice(index, 1); + + await this.client.configManager.write(); + await this.success(message, `The given message has been unblocked.`); + break; + + case "list": + { + const messages: string[] = this.client.configManager.config[message.guildId!]?.message_filter?.data?.blocked_messages ?? []; + const safeMessages: string[][] = []; + let length = 0; + + for (const unsafeMessage of messages) { + if (safeMessages.length === 0) safeMessages.push([]); + + const theMessage = escapeMarkdown(unsafeMessage); + + if (length + theMessage.length >= 3000) { + safeMessages.push([theMessage]); + length = theMessage.length; + continue; + } + + const index = safeMessages.length - 1; + + safeMessages[index].push(theMessage); + length += theMessage.length; + } + + const pagination = new Pagination(safeMessages, { + channelId: message.channelId!, + guildId: message.guildId!, + limit: 1, + timeout: 120_000, + userId: message.member!.user.id, + client: this.client, + embedBuilder({ currentPage, data, maxPages }) { + return new EmbedBuilder({ + author: { + name: `Blocked messages in ${message.guild!.name}`, + iconURL: message.guild!.iconURL() ?? undefined + }, + color: 0x007bff, + description: "`" + data[0].join("`, `") + "`", + footer: { + text: `Page ${currentPage} of ${maxPages}` + } + }); + } + }); + + let reply = await this.deferredReply(message, await pagination.getMessageOptions()); + await pagination.start(reply); + } + + break; + } + } +} diff --git a/src/commands/settings/BlockedTokenCommand.ts b/src/commands/settings/BlockedTokenCommand.ts index 42918ceec..035e6e467 100644 --- a/src/commands/settings/BlockedTokenCommand.ts +++ b/src/commands/settings/BlockedTokenCommand.ts @@ -85,7 +85,8 @@ export default class BlockedTokenCommand extends Command { this.client.configManager.config[guildId!]!.message_filter!.data ??= { blocked_tokens: [], - blocked_words: [] + blocked_words: [], + blocked_messages: [] }; this.client.configManager.config[guildId!]!.message_filter!.data!.blocked_tokens ??= []; diff --git a/src/commands/settings/BlockedWordCommand.ts b/src/commands/settings/BlockedWordCommand.ts index 0bc29f77d..ae0b4d90d 100644 --- a/src/commands/settings/BlockedWordCommand.ts +++ b/src/commands/settings/BlockedWordCommand.ts @@ -85,7 +85,8 @@ export default class BlockedWordCommand extends Command { this.client.configManager.config[guildId!]!.message_filter!.data ??= { blocked_tokens: [], - blocked_words: [] + blocked_words: [], + blocked_messages: [] }; this.client.configManager.config[guildId!]!.message_filter!.data!.blocked_words ??= []; diff --git a/src/services/LoggerService.ts b/src/services/LoggerService.ts index 44a87c3a6..876167566 100644 --- a/src/services/LoggerService.ts +++ b/src/services/LoggerService.ts @@ -1080,16 +1080,36 @@ export default class LoggerService extends Service { }); } - async logBlockedWordOrToken({ guild, user, isToken, token, word, content }: BlockedTokenOrWordOptions) { + async logBlockedWordOrToken({ guild, user, blockType, token, word, message, content }: BlockedTokenOrWordOptions) { + let value: string; + let title: string; + + switch (blockType) { + case 'token': + value = `||${escapeMarkdown(token!)}||`; + title = 'Posted blocked token(s)'; + break; + case 'word': + value = `||${escapeMarkdown(word!)}||`; + title = 'Posted blocked word(s)'; + break; + case 'message': + value = `||${escapeMarkdown(message!)}||`; + title = 'Posted blocked message(s)'; + break; + default: + return; + } + this.sendLogEmbed(guild, { user, - title: `Posted blocked ${isToken ? "token" : "word"}(s)`, + title, footerText: "AutoMod", color: Colors.Yellow, fields: [ { - name: isToken ? "Token" : "Word", - value: `||${escapeMarkdown((isToken ? token : word)!)}||` + name: blockType.toUpperCase(), + value, } ], options: { @@ -1218,9 +1238,10 @@ interface CommonUserActionOptions { } interface BlockedTokenOrWordOptions { - isToken: boolean; + blockType: "token" | "word" | "message"; token?: string; word?: string; + message?: string; guild: Guild; user: User; content: string; diff --git a/src/types/GuildConfigSchema.ts b/src/types/GuildConfigSchema.ts index 111a98dda..61df7709a 100644 --- a/src/types/GuildConfigSchema.ts +++ b/src/types/GuildConfigSchema.ts @@ -118,7 +118,8 @@ export const GuildConfigSchema = z.object({ .or( z.object({ blocked_words: z.boolean().default(false), - blocked_tokens: z.boolean().default(false) + blocked_tokens: z.boolean().default(false), + blocked_messages: z.boolean().default(false), }) ) .default(false), @@ -127,16 +128,18 @@ export const GuildConfigSchema = z.object({ .or( z.object({ blocked_words: z.boolean().default(false), - blocked_tokens: z.boolean().default(false) + blocked_tokens: z.boolean().default(false), + blocked_messages: z.boolean().default(false), }) ) .default(false), data: z .object({ blocked_words: z.array(z.string()).optional().default([]), - blocked_tokens: z.array(z.string()).optional().default([]) - }) - .default({}) + blocked_tokens: z.array(z.string()).optional().default([]), + blocked_messages: z.array(z.string()).optional().default([]), + }) + .default({}) }) .optional(), antispam: z