aboutsummaryrefslogtreecommitdiff
path: root/src/lib/common/AutoMod.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/common/AutoMod.ts')
-rw-r--r--src/lib/common/AutoMod.ts246
1 files changed, 246 insertions, 0 deletions
diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts
new file mode 100644
index 0000000..312beb3
--- /dev/null
+++ b/src/lib/common/AutoMod.ts
@@ -0,0 +1,246 @@
+import { GuildMember, MessageActionRow, MessageButton, MessageEmbed, TextChannel } from 'discord.js';
+import badLinksArray from '../badlinks';
+import badLinksSecretArray from '../badlinks-secret'; // I cannot make this public so just make a new file that export defaults an empty array
+import badWords from '../badwords';
+import { BushButtonInteraction } from '../extensions/discord.js/BushButtonInteraction';
+import { BushMessage } from '../extensions/discord.js/BushMessage';
+import { Moderation } from './Moderation';
+
+export class AutoMod {
+ private message: BushMessage;
+
+ public constructor(message: BushMessage) {
+ this.message = message;
+ if (message.author.id === client.user?.id) return;
+ void this.handle();
+ }
+
+ private async handle(): Promise<void> {
+ if (this.message.channel.type === 'DM' || !this.message.guild) return;
+ if (!(await this.message.guild.hasFeature('automod'))) return;
+
+ const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? {};
+ const badLinks: BadWords = {};
+
+ const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])];
+
+ uniqueLinks.forEach((link) => {
+ badLinks[link] = {
+ severity: Severity.PERM_MUTE,
+ ignoreSpaces: true,
+ ignoreCapitalization: true,
+ reason: 'malicious link'
+ };
+ });
+
+ const result = {
+ ...this.checkWords(customAutomodPhrases),
+ ...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? {} : badWords),
+ ...this.checkWords((await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? {} : badLinks)
+ };
+
+ if (Object.keys(result).length === 0) return;
+
+ const highestOffence = Object.entries(result)
+ .map(([key, value]) => ({ word: key, ...value }))
+ .sort((a, b) => b.severity - a.severity)[0];
+
+ if (highestOffence.severity === undefined || highestOffence.severity === null)
+ void this.message.guild.sendLogChannel('error', {
+ embeds: [
+ {
+ title: 'AutoMod Error',
+ description: `Unable to find severity information for ${util.format.inlineCode(highestOffence.word)}`,
+ color: util.colors.error
+ }
+ ]
+ });
+ else {
+ const color = this.punish(highestOffence);
+ void this.log(highestOffence, color, result);
+ }
+ }
+
+ private checkWords(words: BadWords): BadWords {
+ if (Object.keys(words).length === 0) return {};
+
+ const matchedWords: BadWords = {};
+ for (const word in words) {
+ const wordOptions = words[word];
+ if (this.format(this.message.content, wordOptions).includes(this.format(word, wordOptions))) {
+ matchedWords[word] = wordOptions;
+ }
+ }
+ return matchedWords;
+ }
+
+ private format(string: string, wordOptions: BadWordDetails) {
+ const temp = wordOptions.ignoreCapitalization ? string.toLowerCase() : string;
+ return wordOptions.ignoreSpaces ? temp.replace(/ /g, '') : temp;
+ }
+
+ private punish(highestOffence: BadWordDetails & { word: string }) {
+ let color;
+ switch (highestOffence.severity) {
+ case Severity.DELETE: {
+ color = util.colors.lightGray;
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ break;
+ }
+ case Severity.WARN: {
+ color = util.colors.yellow;
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.message.member?.warn({
+ moderator: this.message.guild!.me!,
+ reason: `[AutoMod] ${highestOffence.reason}`
+ });
+ break;
+ }
+ case Severity.TEMP_MUTE: {
+ color = util.colors.orange;
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.message.member?.mute({
+ moderator: this.message.guild!.me!,
+ reason: `[AutoMod] ${highestOffence.reason}`,
+ duration: 900_000 // 15 minutes
+ });
+ break;
+ }
+ case Severity.PERM_MUTE: {
+ color = util.colors.red;
+ void this.message.delete().catch((e) => deleteError.bind(this, e));
+ void this.message.member?.mute({
+ moderator: this.message.guild!.me!,
+ reason: `[AutoMod] ${highestOffence.reason}`,
+ duration: 0 // permanent
+ });
+ break;
+ }
+ default: {
+ throw new Error(`Invalid severity: ${highestOffence.severity}`);
+ }
+ }
+
+ return color;
+
+ async function deleteError(this: AutoMod, e: Error | any) {
+ this.message.guild?.sendLogChannel('error', {
+ embeds: [
+ {
+ title: 'AutoMod Error',
+ description: `Unable to delete triggered message.`,
+ fields: [{ name: 'Error', value: await util.codeblock(`${e.stack ?? e}`, 1024, 'js', true) }],
+ color: util.colors.error
+ }
+ ]
+ });
+ }
+ }
+
+ private async log(highestOffence: BadWordDetails & { word: string }, color: `#${string}`, offences: BadWords) {
+ void client.console.info(
+ 'autoMod',
+ `Severity <<${highestOffence.severity}>> action performed on <<${this.message.author.tag}>> (<<${
+ this.message.author.id
+ }>>) in <<#${(this.message.channel as TextChannel).name}>> in <<${this.message.guild!.name}>>`
+ );
+
+ return await this.message.guild!.sendLogChannel('automod', {
+ embeds: [
+ new MessageEmbed()
+ .setTitle(`[Severity ${highestOffence.severity}] Automod Action Performed`)
+ .setDescription(
+ `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From**: <#${
+ this.message.channel.id
+ }> [Jump to context](${this.message.url})\n**Blacklisted Words:** ${Object.keys(offences)
+ .map((key) => `\`${key}\``)
+ .join(', ')}`
+ )
+ .addField('Message Content', `${await util.codeblock(this.message.content, 1024)}`)
+ .setColor(color)
+ .setTimestamp()
+ ],
+ components:
+ highestOffence.severity >= 2
+ ? [
+ new MessageActionRow().addComponents(
+ new MessageButton()
+ .setStyle('DANGER')
+ .setLabel('Ban User')
+ .setCustomId(`automod;ban;${this.message.author.id};${highestOffence.reason}`)
+ )
+ ]
+ : undefined
+ });
+ }
+
+ public static async handleInteraction(interaction: BushButtonInteraction) {
+ if (!interaction.memberPermissions?.has('BAN_MEMBERS'))
+ return interaction.reply({
+ content: `${util.emojis.error} You are missing the **Ban Members** permission.`,
+ ephemeral: true
+ });
+ const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';');
+ switch (action) {
+ case 'ban': {
+ const victim = await interaction.guild!.members.fetch(userId);
+ const moderator =
+ interaction.member instanceof GuildMember
+ ? interaction.member
+ : await interaction.guild!.members.fetch(interaction.user.id);
+
+ const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true;
+
+ if (check !== true)
+ return interaction.reply({
+ content: check,
+ ephemeral: true
+ });
+
+ const result = await interaction.guild?.bushBan({
+ user: userId,
+ reason,
+ moderator: interaction.user.id,
+ evidence: (interaction.message as BushMessage).url ?? undefined
+ });
+
+ if (result === 'success')
+ return interaction.reply({
+ content: `${util.emojis.success} Successfully banned **${
+ interaction.guild?.members.cache.get(userId)?.user.tag ?? userId
+ }**.`,
+ ephemeral: true
+ });
+ else
+ return interaction.reply({
+ content: `${util.emojis.error} Could not ban **${
+ interaction.guild?.members.cache.get(userId)?.user.tag ?? userId
+ }**: \`${result}\` .`,
+ ephemeral: true
+ });
+ }
+ }
+ }
+}
+
+export const enum Severity {
+ /** Delete message */
+ DELETE,
+ /** Delete message and warn user */
+ WARN,
+ /** Delete message and mute user for 15 minutes */
+ TEMP_MUTE,
+ /** Delete message and mute user permanently */
+ PERM_MUTE
+}
+
+interface BadWordDetails {
+ severity: Severity;
+ ignoreSpaces: boolean;
+ ignoreCapitalization: boolean;
+ reason: string;
+}
+
+export interface BadWords {
+ [key: string]: BadWordDetails;
+}