From c252fd4a1f14d500d732c5c8f02fcb866779de36 Mon Sep 17 00:00:00 2001 From: "Navy.gif" Date: Mon, 28 Mar 2022 01:44:05 +0300 Subject: [PATCH] automod stuff, mentionfilter & wordwatcher actions --- .../components/observers/Automoderation.js | 202 +++++++++++++++--- 1 file changed, 176 insertions(+), 26 deletions(-) diff --git a/src/structure/components/observers/Automoderation.js b/src/structure/components/observers/Automoderation.js index 93d4f88..cb4f24f 100644 --- a/src/structure/components/observers/Automoderation.js +++ b/src/structure/components/observers/Automoderation.js @@ -1,3 +1,4 @@ +/* eslint-disable require-unicode-regexp */ const { inspect } = require('util'); const similarity = require('similarity'); const { stripIndents } = require('common-tags'); @@ -14,6 +15,17 @@ const CONSTANTS = { KICK: Kick, SOFTBAN: Softban, BAN: Ban + }, + ButtonStyles: { + BAN: 'DANGER', + }, + Permissions: { + WARN: 'KICK_MEMBERS', + MUTE: 'MODERATE_MEMBERS', + KICK: 'KICK_MEMBERS', + SOFTBAN: 'KICK_MEMBERS', + BAN: 'BAN_MEMBERS', + DELETE: 'MANAGE_MESSAGES' } }; @@ -41,12 +53,19 @@ module.exports = class AutoModeration extends Observer { ['messageUpdate', this.filterLinks.bind(this)], ['messageCreate', this.filterInvites.bind(this)], ['messageUpdate', this.filterInvites.bind(this)], - ['messageCreate', this.filterMentions.bind(this)] + ['messageCreate', this.filterMentions.bind(this)], + ['interactionCreate', this.flagAction.bind(this)] ]; this.whitelist = new BinaryTree(this.client, FilterPresets.whitelist); this.executing = {}; + this.regex = { + invite: /((discord)?\s*\.?\s*gg\s*|discord(app)?\.com\/invite)\/\s?(?[a-z0-9]+)/i, + mention: /<@!?(?[0-9]{18,22})>/, + mentionG: /<@!?(?[0-9]{18,22})>/g, + }; + } async _moderate(action, wrapper, channel, member, reason, filterResult) { @@ -118,7 +137,7 @@ module.exports = class AutoModeration extends Observer { // matcher: what gets shown in the message logs | // _matcher: locally used variable for which word in the list triggered it | // type: which detection type matched it - let filterResult = { filter: 'word', match: null, matched: false, matcher: null, _matcher: null, preset: false }; + let filterResult = { filter: 'word', match: null, matched: false, matcher: null, _matcher: null }; const words = content.toLowerCase().replace(/[,?.!]/gu, '').split(' ').filter((elem) => elem.length); // Remove any potential bypass characters //const _words = words.map((word) => word.replace(/[.'*_?+"#%&=-]/gu, '')); @@ -235,7 +254,9 @@ module.exports = class AutoModeration extends Observer { // 5. Remove message, inline response and add a reason to msg object if (!filterResult.matched) return; msg.filtered = filterResult; + filterResult.filter = 'word'; log += `\nFilter result: ${inspect(filterResult)}`; + if (!silent) { const res = await this.client.rateLimiter.limitSend(msg.channel, wrapper.format('W_FILTER_DELETE', { user: author.id }), undefined, 'wordFilter').catch(catcher(255)); //const res = await msg.formattedRespond('W_FILTER_DELETE', { params: { user: author.id } }); @@ -244,6 +265,7 @@ module.exports = class AutoModeration extends Observer { res.delete().catch(() => { /**/ }); }, 10000); } + this.client.rateLimiter.queueDelete(msg.channel, msg).catch(catcher(269)); // 6. Automated actions if (actions.length) { @@ -258,19 +280,16 @@ module.exports = class AutoModeration extends Observer { return act.trigger === 'generic'; }); + if (!action) { this.logger.debug(log); - return this.client.rateLimiter.queueDelete(msg.channel, msg).catch(catcher(275)); + return; } - this.client.rateLimiter.queueDelete(msg.channel, msg).catch(catcher(279)); this.logger.debug(log + '\nSanctioned'); - - filterResult.filter = 'word'; - await this._moderate(action, wrapper, channel, member, wrapper.format('W_FILTER_ACTION'), filterResult, message); + await this._moderate(action, wrapper, channel, member, wrapper.format('W_FILTER_ACTION'), filterResult); } else { - this.client.rateLimiter.queueDelete(msg.channel, msg).catch(catcher(269)); this.logger.debug(log); } @@ -284,7 +303,7 @@ module.exports = class AutoModeration extends Observer { const member = message.member || await guild.members.fetch(author.id).catch(); const settings = await wrapper.settings(); const { wordwatcher: setting } = settings; - const { words, bypass, ignore, channel: _logChannel } = setting; + const { words, bypass, ignore, channel: _logChannel, actions } = setting; const roles = member?.roles.cache.map((r) => r.id) || []; if (!_logChannel || words.length === 0 || roles.some((r) => bypass.includes(r.id)) || ignore.includes(channel.id)) return; @@ -302,11 +321,8 @@ module.exports = class AutoModeration extends Observer { let match = null; for (const reg of words) { - match = content.match(new RegExp(`(?:^|\\s)(${reg})`, 'iu')); - if (match) break; - } if (!match) return; @@ -315,7 +331,7 @@ module.exports = class AutoModeration extends Observer { const embed = { title: `⚠️ Word trigger in **#${channel.name}**`, description: stripIndents` - **[Jump to message](${msg.link})** + **[Jump to message](${msg.url})** `, // ** User:** <@${ author.id }> color: 15120384, fields: context.reverse().reduce((acc, val) => { @@ -333,12 +349,109 @@ module.exports = class AutoModeration extends Observer { }; // TODO: Add action buttons - const sent = await logChannel.send({ embeds: [embed] }).catch((err) => { + const components = []; + for (const action of actions) { + components.push({ + type: 'BUTTON', + label: action.type, + customId: `WORDWATCHER_${action.trigger}`, + style: CONSTANTS.ButtonStyles[action.type] || 'PRIMARY' + }); + } + + const actionRow = components.length ? [{ + type: 'ACTION_ROW', + components + }] : undefined; + const sent = await logChannel.send({ + embeds: [embed], components: actionRow }).catch((err) => { this.logger.error('Error in message flag:\n' + err.stack); }); + + await this.client.storageManager.mongodb.wordwatcher.insertOne({ + message: sent.id, + target: msg.id, + channel: msg.channel.id, + timestamp: Date.now() + }); } + async flagAction(interaction) { + + if (!interaction.isButton() || !interaction.inGuild()) return; + const { guild, message, customId: _actionType, member: moderator } = interaction; + const { components } = message; + const [, actionType] = _actionType.split('_'); + const { permissions } = this.client; + + const settings = await guild.settings(); + const { wordwatcher } = settings; + const { actions } = wordwatcher; + + await interaction.deferUpdate(); + + const fail = (index, opts) => { + this.client.emit('wordWatcherError', { + warning: true, guild, + message: guild.format(index, opts) + }); + components[0].components.find((comp) => comp.customId === _actionType).style = 'DANGER'; + return message.edit({ components }); + }; + + // TODO: finish perm checks, need to figure out a workaround for `command:delete` + //actionType === 'DELETE' ? + // { error: false } : + const inhibitor = + await permissions.execute( + interaction, + { resolveable: `command:${actionType.toLowerCase()}` }, + [CONSTANTS.Permissions[actionType]] + ); + + console.log(inhibitor); + // return; + + const log = await this.client.storageManager.mongodb.wordwatcher.findOne({ + message: message.id + }); + if (!log) return fail('WORDWATCHER_MISSING_LOG'); + + const action = actions.find((act) => act.trigger === actionType); + if (!action) return fail('WORDWATCHER_MISSING_ACTION', { actionType }); + + const targetChannel = await guild.resolveChannel(log.channel).catch(() => null); + if (!targetChannel) return fail('WORDWATCHER_MISSING_CHANNEL'); + + const filterObj = { + filter: 'wordwatcher', + action: action.type + }; + const msg = await targetChannel.messages.fetch(log.target).catch(() => null); + if (msg) { + await msg.delete(); + msg.filtered = filterObj; + } + + await this.client.storageManager.mongodb.wordwatcher.deleteOne({ + message: message.id + }); + + let success = false; + if (action.type !== 'DELETE') { + const member = msg.member || await guild.members.fetch(msg.author.id).catch(() => null); + if (member) + success = await this._moderate(action, guild, targetChannel, member, guild.format('WORDWATCHER_ACTION'), filterObj); + else + this.client.emit('wordWatcherError', { warning: true, guild, message: guild.format('WORDWATCHER_MISSING_MEMBER', { actionType }) }); + } + + components[0].components.find((comp) => comp.customId === _actionType).style = success ? 'SUCCESS' : 'SECONDARY'; + await message.edit({ components }).catch(() => null); + + } + async filterLinks(message, edited) { const { guild, author, channel, guildWrapper: wrapper } = message; @@ -391,6 +504,8 @@ module.exports = class AutoModeration extends Observer { if (!remove) return; msg.filtered = filterResult; + filterResult.filter = 'link'; + if (!silent) { const res = await this.client.rateLimiter.limitSend(msg.channel, wrapper.format('L_FILTER_DELETE', { user: author.id }), undefined, 'linkFilter'); //const res = await msg.formattedRespond(`L_FILTER_DELETE`, { params: { user: author.id } }); @@ -414,12 +529,12 @@ module.exports = class AutoModeration extends Observer { if (!action) this.client.rateLimiter.queueDelete(msg.channel, msg); - msg.filtered.sanctioned = true; + // msg.filtered.sanctioned = true; this.client.rateLimiter.queueDelete(msg.channel, msg); //msg.delete(); filterResult.filter = 'link'; - this._moderate(action, guild, channel, member, wrapper.format('L_FILTER_ACTION', { domain: filterResult.match }), filterResult, message); + await this._moderate(action, guild, channel, member, wrapper.format('L_FILTER_ACTION', { domain: filterResult.match }), filterResult, message); } else this.client.rateLimiter.queueDelete(msg.channel, msg); //msg.delete(); @@ -443,11 +558,10 @@ module.exports = class AutoModeration extends Observer { const { content } = msg; if (!content) return; - const reg = /((discord)?\s?\.?\s?gg\s?|discord(app)?\.com\/invite)\/\s?(?[a-z0-9]+)/iu; - const match = content.match(reg); + const match = content.match(this.regex.invite); if (!match) return; - const result = await guild.checkInvite(match.groups.code); + const result = await wrapper.checkInvite(match.groups.code); if (!result) { // Doesn't resolve to the origin server let action = null; @@ -455,7 +569,8 @@ module.exports = class AutoModeration extends Observer { msg.filtered = { match: match[0], - matcher: 'invites' + matcher: 'invites', + filter: 'invite' }; if (!action) return this.client.rateLimiter.queueDelete(msg.channel, msg); //msg.delete(); if (!silent) { @@ -465,10 +580,10 @@ module.exports = class AutoModeration extends Observer { res.delete().catch(() => { /**/ }); }, 10000); } - msg.filtered.sactioned = true; + // msg.filtered.sactioned = true; this.client.rateLimiter.queueDelete(msg.channel, msg); - this._moderate(action, guild, channel, member, wrapper.format('I_FILTER_ACTION'), { filtered: msg.filtered }, message); + await this._moderate(action, guild, channel, member, wrapper.format('I_FILTER_ACTION'), { filtered: msg.filtered }, message); } @@ -481,16 +596,51 @@ module.exports = class AutoModeration extends Observer { const member = message.member || await guild.members.fetch(author.id).catch(); const settings = await wrapper.settings(); - const { mentionfilter: setting, modpoints } = settings; - const { bypass, ignore, enabled, silent, unique, mentions, actions } = setting; + const { mentionfilter: setting } = settings; + const { bypass, ignore, enabled, silent, unique, limit, actions } = setting; const roles = member?.roles.cache.map((r) => r.id) || []; if (!enabled || roles.some((r) => bypass.includes(r)) || ignore.includes(channel.id)) return; - const reg = /<@!?[0-9]{18,22}>/gu; const { content } = message; if (!content) return; - //const mentions = content.match(reg); + + const matches = content.match(this.regex.mentionG); + if (!matches) return; + + let ids = matches.map((match) => match.match(this.regex.mention).groups.id); + if (unique) { + const set = new Set(ids); + ids = []; + for (const id of set.values()) ids.push(id); + } + + if (ids.length < limit) return; + if (!silent) { + const res = await this.client.rateLimiter.limitSend(channel, wrapper.format('M_FILTER_DELETE', { user: author.id }), undefined, 'mentionFilter'); + setTimeout(() => { + res.delete().catch(() => { /**/ }); + }, 10000); + } + + this.client.rateLimiter.queueDelete(channel, message); + const filterResult = { + filter: 'mention', + amount: ids.length + }; + message.filtered = filterResult; + + if (actions.length) { + + const action = actions.find((act) => { + return act.trigger === 'generic'; + }); + + if (!action) return; + + await this._moderate(action, wrapper, channel, member, wrapper.format('M_FILTER_ACTION'), filterResult); + + } }