From d29acc82b1797d9d9bcc4056317fe9d7ae902cc2 Mon Sep 17 00:00:00 2001 From: Julian Vennen Date: Mon, 8 Jul 2024 20:05:27 +0200 Subject: [PATCH] Move dm punishment to special column --- src/automod/AutoModManager.js | 10 +++- src/bot/Database.js | 4 +- .../settings/bad-word/AddBadWordCommand.js | 55 ++++++++++++------ .../settings/bad-word/EditBadWordCommand.js | 53 +++++++++++------ src/database/BadWord.js | 58 ++++++++++++++----- src/database/ChatTriggeredFeature.js | 15 ++--- src/database/Punishment.js | 2 + src/database/migrations/DMMigration.js | 16 +++++ src/database/migrations/Migration.js | 2 + src/discord/MemberWrapper.js | 6 +- src/embeds/KeyValueEmbed.js | 26 +++++++++ 11 files changed, 180 insertions(+), 67 deletions(-) create mode 100644 src/database/migrations/DMMigration.js diff --git a/src/automod/AutoModManager.js b/src/automod/AutoModManager.js index 2c3ff7248..9f19c1d39 100644 --- a/src/automod/AutoModManager.js +++ b/src/automod/AutoModManager.js @@ -1,4 +1,4 @@ -import {Collection, PermissionFlagsBits, RESTJSONErrorCodes, ThreadChannel, userMention} from 'discord.js'; +import {bold, Collection, PermissionFlagsBits, RESTJSONErrorCodes, ThreadChannel, userMention} from 'discord.js'; import GuildSettings from '../settings/GuildSettings.js'; import bot from '../bot/Bot.js'; import MemberWrapper from '../discord/MemberWrapper.js'; @@ -217,13 +217,17 @@ export class AutoModManager { const reason = 'Using forbidden words or phrases'; const comment = `(Filter ID: ${word.id})`; await bot.delete(message, reason + ' ' + comment); - if (word.response !== 'disabled' && word.punishment.action.toLowerCase() !== 'dm') { + if (word.response !== 'disabled') { await this.#sendWarning(message, word.getResponse()); } + + const member = new Member(message.author, message.guild); if (word.punishment.action !== 'none') { - const member = new Member(message.author, message.guild); await member.executePunishment(word.punishment, reason, comment); } + if (word.dm) { + await member.guild.sendDM(member.user, `Your message in ${bold(message.guild.name)} was removed: ` + word.dm); + } } /** diff --git a/src/bot/Database.js b/src/bot/Database.js index 3f5d85f38..8b1316d0e 100644 --- a/src/bot/Database.js +++ b/src/bot/Database.js @@ -5,6 +5,7 @@ import CommentFieldMigration from '../database/migrations/CommentFieldMigration. import {asyncFilter} from '../util/util.js'; import BadWordVisionMigration from '../database/migrations/BadWordVisionMigration.js'; import AutoResponseVisionMigration from '../database/migrations/AutoResponseVisionMigration.js'; +import DMMigration from '../database/migrations/DMMigration.js'; export class Database { /** @@ -103,7 +104,7 @@ export class Database { await this.query('CREATE TABLE IF NOT EXISTS `guilds` (`id` VARCHAR(20) NOT NULL, `config` TEXT NOT NULL, PRIMARY KEY (`id`))'); await this.query('CREATE TABLE IF NOT EXISTS `users` (`id` VARCHAR(20) NOT NULL, `config` TEXT NOT NULL, PRIMARY KEY (`id`))'); await this.query('CREATE TABLE IF NOT EXISTS `responses` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `trigger` TEXT NOT NULL, `response` TEXT NOT NULL, `global` BOOLEAN NOT NULL, `channels` TEXT NULL DEFAULT NULL, `enableVision` BOOLEAN DEFAULT FALSE)'); - await this.query('CREATE TABLE IF NOT EXISTS `badWords` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `trigger` TEXT NOT NULL, `punishment` TEXT NOT NULL, `response` TEXT NOT NULL, `global` BOOLEAN NOT NULL, `channels` TEXT NULL DEFAULT NULL, `priority` int NULL, `enableVision` BOOLEAN DEFAULT FALSE)'); + await this.query('CREATE TABLE IF NOT EXISTS `badWords` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `trigger` TEXT NOT NULL, `punishment` TEXT NOT NULL, `response` TEXT NOT NULL, `global` BOOLEAN NOT NULL, `channels` TEXT NULL DEFAULT NULL, `priority` int NULL, `dm` TEXT NULL DEFAULT NULL, `enableVision` BOOLEAN DEFAULT FALSE)'); await this.query('CREATE TABLE IF NOT EXISTS `moderations` (`id` int PRIMARY KEY AUTO_INCREMENT, `guildid` VARCHAR(20) NOT NULL, `userid` VARCHAR(20) NOT NULL, `action` VARCHAR(10) NOT NULL, `created` bigint NOT NULL, `value` int DEFAULT 0, `expireTime` bigint NULL DEFAULT NULL, `reason` TEXT, `comment` TEXT NULL DEFAULT NULL, `moderator` VARCHAR(20) NULL DEFAULT NULL, `active` BOOLEAN DEFAULT TRUE)'); await this.query('CREATE TABLE IF NOT EXISTS `confirmations` (`id` int PRIMARY KEY AUTO_INCREMENT, `data` TEXT NOT NULL, `expires` bigint NOT NULL)'); await this.query('CREATE TABLE IF NOT EXISTS `safeSearch` (`hash` CHAR(64) PRIMARY KEY, `data` TEXT NOT NULL)'); @@ -114,6 +115,7 @@ export class Database { new CommentFieldMigration(this), new BadWordVisionMigration(this), new AutoResponseVisionMigration(this), + new DMMigration(this), ], async migration => await migration.check()); } diff --git a/src/commands/settings/bad-word/AddBadWordCommand.js b/src/commands/settings/bad-word/AddBadWordCommand.js index d8a429d74..b460113d7 100644 --- a/src/commands/settings/bad-word/AddBadWordCommand.js +++ b/src/commands/settings/bad-word/AddBadWordCommand.js @@ -40,9 +40,6 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { }, { name: 'Strike user', value: 'strike' - }, { - name: 'Send direct message', - value: 'DM' } ) ); @@ -93,12 +90,25 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { new TextInputBuilder() .setRequired(false) .setCustomId('priority') - .setStyle(TextInputStyle.Paragraph) + .setStyle(TextInputStyle.Short) .setPlaceholder('0') .setLabel('Priority') .setMinLength(1) .setMaxLength(10) - ) + ), + /** @type {*} */ + new ActionRowBuilder() + .addComponents( + /** @type {*} */ + new TextInputBuilder() + .setRequired(false) + .setCustomId('dm') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('This is a direct message sent to the user when their message was deleted') + .setLabel('Direct Message') + .setMinLength(1) + .setMaxLength(3000) + ), ); if (['ban', 'mute'].includes(punishment)) { @@ -131,20 +141,25 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { return; } - let trigger, response = null, duration = null, priority = 0; + let trigger, response = null, duration = null, priority = 0, dm = null; for (let component of interaction.components) { component = component.components[0]; - if (component.customId === 'trigger') { - trigger = component.value; - } - else if (component.customId === 'response') { - response = component.value?.substring?.(0, 4000); - } - else if (component.customId === 'duration') { - duration = parseTime(component.value) || null; - } - else if (component.customId === 'priority') { - priority = parseInt(component.value) || 0; + switch (component.customId) { + case 'trigger': + trigger = component.value; + break; + case 'response': + response = component.value?.substring?.(0, 4000); + break; + case 'duration': + duration = parseTime(component.value) || null; + break; + case 'priority': + priority = parseInt(component.value) || 0; + break; + case 'dm': + dm = component.value?.substring?.(0, 3000); + break; } } @@ -160,6 +175,7 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { confirmation.data.punishment, duration, priority, + dm, confirmation.data.vision, ); } else { @@ -167,6 +183,7 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { confirmation.data.response = response; confirmation.data.duration = duration; confirmation.data.priority = priority; + confirmation.data.dm = dm; confirmation.expires = timeAfter('30 min'); await interaction.reply({ @@ -209,6 +226,7 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { confirmation.data.punishment, confirmation.data.duration, confirmation.data.priority, + confirmation.data.dm, confirmation.data.vision, ); } @@ -225,6 +243,7 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { * @param {?string} punishment * @param {?number} duration * @param {?number} priority + * @param {?string} dm * @param {?boolean} enableVision * @return {Promise<*>} */ @@ -238,6 +257,7 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { punishment, duration, priority, + dm, enableVision, ) { const result = await BadWord.new( @@ -250,6 +270,7 @@ export default class AddBadWordCommand extends AddAutoResponseCommand { punishment, duration, priority, + dm, enableVision, ); if (!result.success) { diff --git a/src/commands/settings/bad-word/EditBadWordCommand.js b/src/commands/settings/bad-word/EditBadWordCommand.js index 4c051a99f..80cc5ee08 100644 --- a/src/commands/settings/bad-word/EditBadWordCommand.js +++ b/src/commands/settings/bad-word/EditBadWordCommand.js @@ -71,9 +71,6 @@ export default class EditBadWordCommand extends CompletingBadWordCommand { }, { name: 'Strike user', value: 'strike' - }, { - name: 'Send direct message', - value: 'DM' } ) ); @@ -186,13 +183,26 @@ export default class EditBadWordCommand extends CompletingBadWordCommand { new TextInputBuilder() .setRequired(false) .setCustomId('priority') - .setStyle(TextInputStyle.Paragraph) + .setStyle(TextInputStyle.Short) .setPlaceholder('0') .setLabel('Priority') .setValue(badWord.priority.toString()) .setMinLength(1) .setMaxLength(10) - ) + ), + /** @type {*} */ + new ActionRowBuilder() + .addComponents( + /** @type {*} */ + new TextInputBuilder() + .setRequired(false) + .setCustomId('dm') + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder('This is a direct message sent to the user when their message was deleted') + .setLabel('Direct Message') + .setMinLength(1) + .setMaxLength(3000) + ), ); if (['ban', 'mute'].includes(punishment)) { @@ -234,20 +244,25 @@ export default class EditBadWordCommand extends CompletingBadWordCommand { return; } - let trigger, response, duration = null, priority; + let trigger, response, duration = null, priority = null, dm = null; for (let component of interaction.components) { component = component.components[0]; - if (component.customId === 'trigger') { - trigger = component.value; - } - else if (component.customId === 'response') { - response = component.value; - } - else if (component.customId === 'duration') { - duration = parseTime(component.value) || null; - } - else if (component.customId === 'priority') { - priority = parseInt(component.value) || 0; + switch (component.customId) { + case 'trigger': + trigger = component.value; + break; + case 'response': + response = component.value?.substring?.(0, 4000); + break; + case 'duration': + duration = parseTime(component.value) || null; + break; + case 'priority': + priority = parseInt(component.value) || 0; + break; + case 'dm': + dm = component.value?.substring?.(0, 3000); + break; } } @@ -264,6 +279,7 @@ export default class EditBadWordCommand extends CompletingBadWordCommand { confirmation.data.punishment, duration, priority, + dm, confirmation.data.vision, ); } else { @@ -331,6 +347,7 @@ export default class EditBadWordCommand extends CompletingBadWordCommand { * @param {PunishmentAction} punishment * @param {?number} duration * @param {number} priority + * @param {?string} dm * @param {?boolean} vision * @return {Promise<*>} */ @@ -345,6 +362,7 @@ export default class EditBadWordCommand extends CompletingBadWordCommand { punishment, duration, priority, + dm, vision, ) { const badWord = /** @type {?BadWord} */ @@ -364,6 +382,7 @@ export default class EditBadWordCommand extends CompletingBadWordCommand { } badWord.trigger = triggerResponse.trigger; badWord.response = response || 'disabled'; + badWord.dm = dm || 'disabled'; badWord.punishment = new Punishment({action: punishment, duration}); badWord.priority = priority; await badWord.save(); diff --git a/src/database/BadWord.js b/src/database/BadWord.js index d49c24dcd..961a7b928 100644 --- a/src/database/BadWord.js +++ b/src/database/BadWord.js @@ -1,22 +1,30 @@ import ChatTriggeredFeature from './ChatTriggeredFeature.js'; import TypeChecker from '../settings/TypeChecker.js'; import {channelMention} from 'discord.js'; -import Punishment from './Punishment.js'; +import Punishment, {PunishmentAction} from './Punishment.js'; import colors from '../util/colors.js'; import ChatFeatureEmbed from '../embeds/ChatFeatureEmbed.js'; +import {EMBED_FIELD_LIMIT} from '../util/apiLimits.js'; /** * Class representing a bad word */ export default class BadWord extends ChatTriggeredFeature { - - static punishmentTypes = ['none', 'ban', 'kick', 'mute', 'softban', 'strike', 'dm']; - static defaultResponse = 'Your message includes words/phrases that are not allowed here!'; static tableName = 'badWords'; - static columns = ['guildid', 'trigger', 'punishment', 'response', 'global', 'channels', 'priority', 'enableVision']; + static columns = [ + 'guildid', + 'trigger', + 'punishment', + 'response', + 'global', + 'channels', + 'priority', + 'enableVision', + 'dm', + ]; /** * constructor - create a bad word @@ -28,6 +36,7 @@ export default class BadWord extends ChatTriggeredFeature { * @param {Boolean} json.global does this apply to all channels in this guild * @param {import('discord.js').Snowflake[]} [json.channels] channels that this applies to * @param {Number} [json.priority] badword priority (higher -> more important) + * @param {?string} [json.dm] direct message to send to the user * @param {boolean} [json.enableVision] enable vision api for this badword * @param {Number} [id] id in DB * @return {BadWord} @@ -39,22 +48,29 @@ export default class BadWord extends ChatTriggeredFeature { if (json) { this.punishment = typeof (json.punishment) === 'string' ? JSON.parse(json.punishment) : json.punishment; this.response = json.response; - if (this.punishment?.action?.toUpperCase?.() === 'DM' && this.response && this.response !== 'disabled') { - if (this.response === 'default') { - this.punishment.message = BadWord.defaultResponse; - } else { - this.punishment.message = this.response; - } - } this.global = json.global; this.channels = json.channels; this.priority = json.priority || 0; this.enableVision = json.enableVision ?? false; + this.dm = json.dm; } if (!this.channels) { this.channels = []; } + + // Temporary for migrating dm 'punishments' to the new dm field + if (this.punishment?.action?.toUpperCase?.() === 'DM') { + this.dm = this.punishment.message; + if (this.response === 'default') { + this.dm ??= BadWord.defaultResponse; + } else { + this.dm ??= this.response; + } + this.punishment.message = undefined; + this.punishment.action = PunishmentAction.NONE; + this.response = 'disabled'; + } } @@ -87,7 +103,17 @@ export default class BadWord extends ChatTriggeredFeature { * @returns {(*|string)[]} */ serialize() { - return [this.gid, JSON.stringify(this.trigger), JSON.stringify(this.punishment), this.response, this.global, this.channels.join(','), this.priority, this.enableVision]; + return [ + this.gid, + JSON.stringify(this.trigger), + JSON.stringify(this.punishment), + this.response, + this.global, + this.channels.join(','), + this.priority, + this.enableVision, + this.dm, + ]; } /** @@ -100,7 +126,8 @@ export default class BadWord extends ChatTriggeredFeature { const duration = this.punishment.duration; return new ChatFeatureEmbed(this, title, color) .addPair('Punishment', `${this.punishment.action} ${duration ? `for ${duration}` : ''}`) - .addPair('Priority', this.priority); + .addPair('Priority', this.priority) + .addFieldIf(this.dm, 'DM', this.dm?.substring(0, EMBED_FIELD_LIMIT)); } /** @@ -114,6 +141,7 @@ export default class BadWord extends ChatTriggeredFeature { * @param {?string} punishment * @param {?number} duration * @param {?number} priority + * @param {?string} dm * @param {?boolean} enableVision * @returns {Promise<{success:boolean, badWord: ?BadWord, message: ?string}>} */ @@ -127,6 +155,7 @@ export default class BadWord extends ChatTriggeredFeature { punishment, duration, priority, + dm, enableVision, ) { let trigger = this.getTrigger(triggerType, triggerContent); @@ -140,6 +169,7 @@ export default class BadWord extends ChatTriggeredFeature { channels, response: response || 'disabled', priority, + dm, enableVision, }); await badWord.save(); diff --git a/src/database/ChatTriggeredFeature.js b/src/database/ChatTriggeredFeature.js index ae32da9bb..f8679d9b8 100644 --- a/src/database/ChatTriggeredFeature.js +++ b/src/database/ChatTriggeredFeature.js @@ -212,7 +212,7 @@ export default class ChatTriggeredFeature { for (const column of columns) { assignments.push(`${database.escapeId(column)}=?`); } - if (data.length !== columns.length) throw 'Unable to update, lengths differ!'; + if (data.length !== columns.length) throw new Error('Unable to update, lengths differ!'); data.push(this.id); await database.queryAll(`UPDATE ${this.constructor.escapedTableName} SET ${assignments.join(', ')} @@ -267,15 +267,10 @@ export default class ChatTriggeredFeature { * @returns {this} */ static fromData(data) { - return new this(data.guildid, { - trigger: JSON.parse(data.trigger), - punishment: data.punishment, - response: data.response, - global: data.global === 1, - channels: data.channels.split(','), - priority: data.priority, - enableVision: data.enableVision, - }, data.id); + data.trigger = JSON.parse(data.trigger); + data.global = data.global === 1; + data.channels = data.channels.split(','); + return new this(data.guildid, data, data.id); } /** diff --git a/src/database/Punishment.js b/src/database/Punishment.js index 8620c35fb..ee8d92ec8 100644 --- a/src/database/Punishment.js +++ b/src/database/Punishment.js @@ -13,6 +13,7 @@ export default class Punishment { /** * @type {?string} + * @deprecated */ message = null; @@ -49,4 +50,5 @@ export const PunishmentAction = { MUTE: 'mute', SOFTBAN: 'softban', STRIKE: 'strike', + NONE: 'none', }; \ No newline at end of file diff --git a/src/database/migrations/DMMigration.js b/src/database/migrations/DMMigration.js new file mode 100644 index 000000000..f1ee7c777 --- /dev/null +++ b/src/database/migrations/DMMigration.js @@ -0,0 +1,16 @@ +import Migration from './Migration.js'; + +export default class DMMigration extends Migration { + + async check() { + /** + * @type {{Field: string, Type: string, Key: string, Default, Extra: string}[]} + */ + const columns = await this.database.queryAll('DESCRIBE `badWords`'); + return !columns.some(column => column.Field === 'dm'); + } + + async run() { + await this.database.query('ALTER TABLE `badWords` ADD COLUMN `dm` TEXT NULL DEFAULT NULL AFTER `priority`'); + } +} diff --git a/src/database/migrations/Migration.js b/src/database/migrations/Migration.js index e19ca1eed..b47b8ad46 100644 --- a/src/database/migrations/Migration.js +++ b/src/database/migrations/Migration.js @@ -15,6 +15,7 @@ export default class Migration { /** * Does the migration need to run? * @returns {Promise} + * @abstract */ async check() { return false; @@ -23,6 +24,7 @@ export default class Migration { /** * Run the migration * @returns {Promise} + * @abstract */ async run() { throw new Error('Migration not implemented'); diff --git a/src/discord/MemberWrapper.js b/src/discord/MemberWrapper.js index 65bda5571..22a5609bc 100644 --- a/src/discord/MemberWrapper.js +++ b/src/discord/MemberWrapper.js @@ -353,12 +353,8 @@ export default class MemberWrapper { case 'strike': return this.strike(reason, comment, this.user.client.user); - case 'dm': - await this.guild.sendDM(this.user, `Your message in ${bold(this.guild.guild.name)} was removed: ` + punishment.message); - return; - default: - throw `Unknown punishment action ${punishment.action}`; + throw new Error(`Unknown punishment action ${punishment.action}`); } } diff --git a/src/embeds/KeyValueEmbed.js b/src/embeds/KeyValueEmbed.js index 61f792af0..349bf17ff 100644 --- a/src/embeds/KeyValueEmbed.js +++ b/src/embeds/KeyValueEmbed.js @@ -51,4 +51,30 @@ export default class KeyValueEmbed extends LineEmbed { } return this; } + + /** + * Add a field - but don't be stupid about types + * @param {string} name + * @param {string} value + * @param {boolean} inline + * @returns {KeyValueEmbed} + */ + addField(name, value, inline = false) { + return this.addFields(/** @type {*} */ {name, value, inline}); + } + + /** + * Add a field if a condition is met + * @param {*} condition + * @param {string} name + * @param {string} value + * @param {boolean} inline + * @returns {KeyValueEmbed} + */ + addFieldIf(condition, name, value, inline = false) { + if (condition) { + this.addField(name, value, inline); + } + return this; + } } \ No newline at end of file