diff --git a/structure/client/components/observers/Automoderation.js b/structure/client/components/observers/Automoderation.js index 6ab994a..ae9c055 100644 --- a/structure/client/components/observers/Automoderation.js +++ b/structure/client/components/observers/Automoderation.js @@ -3,6 +3,7 @@ const similarity = require('similarity'); const { Observer, BinaryTree } = require('../../../interfaces'); const { FilterUtil, FilterPresets } = require('../../../../util'); const { Warn, Mute, Kick, Softban, Ban } = require('../../../moderation/infractions'); +const { stripIndents } = require('common-tags'); const CONSTANTS = { Infractions: { @@ -26,6 +27,8 @@ module.exports = class AutoModeration extends Observer { this.hooks = [ ['message', this.filterWords.bind(this)], ['messageUpdate', this.filterWords.bind(this)], + ['message', this.flagMessages.bind(this)], + ['messageUpdate', this.flagMessages.bind(this)], ['message', this.filterLinks.bind(this)], ['messageUpdate', this.filterLinks.bind(this)], ['message', this.filterInvites.bind(this)], @@ -45,8 +48,8 @@ module.exports = class AutoModeration extends Observer { const member = message.member || await guild.members.fetch(author.id).catch(); const settings = await guild.settings(); const { wordFilter: setting, moderationPoints } = settings; - const { bypass, ignore, enabled, silent, explicit, fuzzy, tokenized, whitelist, actions, presets } = setting; - const roles = member.roles.cache.map((r) => r.id); + const { bypass, ignore, enabled, silent, explicit, fuzzy, regex, whitelist, actions } = setting; + const roles = member?.roles.cache.map((r) => r.id) || []; if (!enabled || roles.some((r) => bypass.includes(r.id)) || ignore.includes(channel.id)) return; @@ -60,38 +63,41 @@ module.exports = class AutoModeration extends Observer { // matched: did it match at all ? | // matcher: what gets shown in the message logs | // _matcher: locally used variable for which word in the list triggered it | - // filter: which filter list was used // type: which detection type matched it - let filterResult = { match: null, matched: false, matcher: null, _matcher: null, preset: false, filter: null }; + let filterResult = { match: null, matched: false, matcher: null, _matcher: null, preset: false }; const words = content.toLowerCase().split(' ').filter((elem) => elem.length); // Remove any potential bypass characters const _words = words.map((word) => word.replace(/[.'*_?+"#%&=-]/gu, '')); // 1. Filter for preset lists - if (presets.length) { - for (const preset of presets) { + // CHANGED BEHAVIOUR OF PRESETS, THEY NOW USE THE REGEX PART OF THE FILTER + // if (presets.length) { + // for (const preset of presets) { - const text = _words.join('').replace(/\s/u, ''); //Also check for spaced out words, ex "f u c k" - //Combine array of presets to one expression - const regex = new RegExp(`(${FilterPresets[preset].join(')|(')})`, 'ui'); - const match = content.match(regex) || text.length === words.length ? text.match(regex) : null; - if (!match) continue; - this.client.logger.debug(`\nMessage matched with "${preset}" preset list.\nMatch: ${match[0]}\nFull content: ${content}`); - filterResult = { - match: match[0], - matched: true, - matcher: preset, - preset, - type: 'preset' - }; - break; + // const text = _words.join('').replace(/\s/u, ''); //Also check for spaced out words, ex "f u c k" + // //Combine array of presets to one expression + // const _regex = new RegExp(`(${FilterPresets[preset].join(')|(')})`, 'ui'); + // const match = content.match(_regex) || text.length === words.length ? text.match(_regex) : null; + // if (!match) continue; + // this.client.logger.debug(`\nMessage matched with "${preset}" preset list.\nMatch: ${match[0]}\nFull content: ${content}`); + // filterResult = { + // match: match[0], + // matched: true, + // matcher: preset, + // preset, + // type: 'preset' + // }; + // break; - } - } + // } + // } // 2. Filter explicit - no bypass checking (unless you count normalising the text, i.e. emoji letters => normal letters) if (explicit.length && !filterResult.matched) { + //filterResult = FilterUtil.filterExplicit(words, explicit); + // if(filterResult) + for (const word of explicit) { //Do it like this instead of regex so it doesn't match stuff like Scunthorpe with cunt if (words.some((_word) => _word === word)) { @@ -103,6 +109,7 @@ module.exports = class AutoModeration extends Observer { _matcher: word, type: 'explicit' }; + break; } } @@ -156,20 +163,23 @@ module.exports = class AutoModeration extends Observer { } - // 4. Filter tokenized - if (tokenized.length && !filterResult.matched) { + // 4. Filter regex + if (regex.length && !filterResult.matched) { - for (const word of tokenized) { + for (const reg of regex) { + + const match = content.match(new RegExp(reg, 'iu')); - if (content.toLowerCase().includes(word)) { - this.client.logger.debug(`\nMessage matched with "${word}" in the tokenized list.\nFull content: ${content}`); + if (match) { + this.client.logger.debug(`\nMessage matched with "${reg}" in the regex list.\nMatch: ${match[0]}\nFull content: ${content}`); filterResult = { - match: word, + match: match[0], matched: true, - _matcher: word, - matcher: 'tokenized', - type: 'tokenized' + _matcher: reg, + matcher: `Regex: __${reg}__`, + type: 'regex' }; + break; } } @@ -201,6 +211,8 @@ module.exports = class AutoModeration extends Observer { msg.filtered.sanctioned = true; await msg.delete(); + + // NOTE: this will have to be changed whenever the moderation manager is finished and properly supports sth like this this.client.moderationManager.handleInfraction( CONSTANTS.Infractions[action.type], { // Hacky patched together message object with just the required stuff for modmanager to work @@ -221,8 +233,8 @@ module.exports = class AutoModeration extends Observer { }, format: guild.format.bind(guild), // eslint-disable-next-line no-empty-function - respond: () => {} - }, + respond: () => { } + }, { targets: [member], reason: msg.format('W_FILTER_ACTION'), @@ -233,8 +245,61 @@ module.exports = class AutoModeration extends Observer { } ); + } else msg.delete(); + + } + + async flagMessages(message, edited) { + + const { guild, author, channel } = message; + if (!guild || author.bot) return; + + const member = message.member || await guild.members.fetch(author.id).catch(); + const settings = await guild.settings(); + const { wordWatcher: setting } = settings; + const { words, bypass, ignore, channel: _logChannel } = 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; + + const logChannel = await guild.resolveChannel(_logChannel); + const msg = edited || message; + const content = FilterUtil.normalize(msg.cleanContent); + let match = null; + + for (const reg of words) { + + match = content.match(new RegExp(reg, 'iu')); + + if (match) break; + } + if (!match) return; + + const context = channel.messages.cache.sort((m1, m2) => m2.createdTimestamp - m1.createdTimestamp).first(5); + const embed = { + title: `⚠️ Word trigger in **#${channel.name}**`, + description: stripIndents` + **[Jump to message](${msg.link})** + `, // ** User:** <@${ author.id }> + color: 15120384, + fields: context.reverse().reduce((acc, val) => { + const text = val.content.length ? val.content.replace(match, '**__$&__**') : '**NO CONTENT**'; + acc.push({ + name: `${val.author.tag} (${val.author.id}) - ${val.id}`, + value: text.length < 1024 ? text : text.substring(0, 1013) + '...' + }); + if (text.length > 1024) acc.push({ + name: `\u200b`, + value: '...' + text.substring(1013, 2034) + }); + return acc; + }, []) + }; + + logChannel.send({ embed }); + } async filterLinks(message, edited) { @@ -242,6 +307,20 @@ module.exports = class AutoModeration extends Observer { } async filterInvites(message, edited) { + + const { guild, author, channel } = message; + if (!guild || author.bot) return; + + const member = message.member || await guild.members.fetch(author.id).catch(); + const settings = await guild.settings(); + const { wordWatcher: setting } = settings; + const { words, bypass, ignore, channel: _logChannel } = 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; + + const msg = edited || message; + }