automod stuff, mentionfilter & wordwatcher actions
This commit is contained in:
parent
d29107cced
commit
c252fd4a1f
@ -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?(?<code>[a-z0-9]+)/i,
|
||||
mention: /<@!?(?<id>[0-9]{18,22})>/,
|
||||
mentionG: /<@!?(?<id>[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,10 +349,107 @@ 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) {
|
||||
@ -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?(?<code>[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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user