From 9817b2e6045df07a4b6195be4ec78a61cac9e74f Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Thu, 21 Oct 2021 00:26:08 -0400 Subject: b --- src/commands/moderation/ban.ts | 2 +- src/commands/moderation/kick.ts | 2 +- src/commands/moderation/mute.ts | 2 +- src/commands/moderation/unmute.ts | 2 +- src/commands/moderation/warn.ts | 2 +- src/lib/common/AutoMod.ts | 246 +++++++++++++++++++++++ src/lib/common/Moderation.ts | 184 +++++++++++++++++ src/lib/common/_autoMod.ts | 246 ----------------------- src/lib/common/_moderation.ts | 184 ----------------- src/lib/extensions/discord.js/BushGuild.ts | 2 +- src/lib/extensions/discord.js/BushGuildMember.ts | 2 +- 11 files changed, 437 insertions(+), 437 deletions(-) create mode 100644 src/lib/common/AutoMod.ts create mode 100644 src/lib/common/Moderation.ts delete mode 100644 src/lib/common/_autoMod.ts delete mode 100644 src/lib/common/_moderation.ts (limited to 'src') diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 2116a64..b3d97d2 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -1,6 +1,6 @@ import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage } from '@lib'; import { Snowflake, User } from 'discord.js'; -import { Moderation } from '../../lib/common/_moderation'; +import { Moderation } from '../../lib/common/Moderation'; export default class BanCommand extends BushCommand { public constructor() { diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 14bd82a..9bd5658 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -1,5 +1,5 @@ import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage, BushUser } from '@lib'; -import { Moderation } from '../../lib/common/_moderation'; +import { Moderation } from '../../lib/common/Moderation'; export default class KickCommand extends BushCommand { public constructor() { diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 0579129..03ecf2a 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -1,5 +1,5 @@ import { AllowedMentions, BushCommand, BushMessage, BushSlashMessage, BushUser } from '@lib'; -import { Moderation } from '../../lib/common/_moderation'; +import { Moderation } from '../../lib/common/Moderation'; export default class MuteCommand extends BushCommand { public constructor() { diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index 967b560..e430b83 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -1,5 +1,5 @@ import { AllowedMentions, BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '@lib'; -import { Moderation } from '../../lib/common/_moderation'; +import { Moderation } from '../../lib/common/Moderation'; export default class UnmuteCommand extends BushCommand { public constructor() { diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 46ca7c9..6ae8442 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -1,5 +1,5 @@ import { BushCommand, BushGuildMember, BushMessage, BushSlashMessage, BushUser } from '@lib'; -import { Moderation } from '../../lib/common/_moderation'; +import { Moderation } from '../../lib/common/Moderation'; export default class WarnCommand extends BushCommand { public constructor() { 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 { + 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; +} diff --git a/src/lib/common/Moderation.ts b/src/lib/common/Moderation.ts new file mode 100644 index 0000000..29d66fa --- /dev/null +++ b/src/lib/common/Moderation.ts @@ -0,0 +1,184 @@ +import { Snowflake } from 'discord-api-types'; +import { + ActivePunishment, + ActivePunishmentType, + BushGuildMember, + BushGuildMemberResolvable, + BushGuildResolvable, + Guild, + ModLog, + ModLogType +} from '..'; + +export class Moderation { + /** + * Checks if a moderator can perform a moderation action on another user. + * @param moderator - The person trying to perform the action. + * @param victim - The person getting punished. + * @param type - The type of punishment - used to format the response. + * @param checkModerator - Whether or not to check if the victim is a moderator. + */ + public static async permissionCheck( + moderator: BushGuildMember, + victim: BushGuildMember, + type: 'mute' | 'unmute' | 'warn' | 'kick' | 'ban' | 'unban' | 'add a punishment role to' | 'remove a punishment role from', + checkModerator = true, + force = false + ): Promise { + if (force) return true; + + // If the victim is not in the guild anymore it will be undefined + if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true; + + if (moderator.guild.id !== victim.guild.id) { + throw new Error('moderator and victim not in same guild'); + } + + const isOwner = moderator.guild.ownerId === moderator.id; + if (moderator.id === victim.id && !type.startsWith('un')) { + return `${util.emojis.error} You cannot ${type} yourself.`; + } + if ( + moderator.roles.highest.position <= victim.roles.highest.position && + !isOwner && + !(type.startsWith('un') && moderator.id === victim.id) + ) { + return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; + } + if ( + victim.roles.highest.position >= victim.guild.me!.roles.highest.position && + !(type.startsWith('un') && moderator.id === victim.id) + ) { + return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; + } + if (checkModerator && victim.permissions.has('MANAGE_MESSAGES') && !(type.startsWith('un') && moderator.id === victim.id)) { + if (await moderator.guild.hasFeature('modsCanPunishMods')) { + return true; + } else { + return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; + } + } + return true; + } + + public static async createModLogEntry( + options: { + type: ModLogType; + user: BushGuildMemberResolvable; + moderator: BushGuildMemberResolvable; + reason: string | undefined | null; + duration?: number; + guild: BushGuildResolvable; + pseudo?: boolean; + evidence?: string; + }, + getCaseNumber = false + ): Promise<{ log: ModLog | null; caseNum: number | null }> { + const user = (await util.resolveNonCachedUser(options.user))!.id; + const moderator = (await util.resolveNonCachedUser(options.moderator))!.id; + const guild = client.guilds.resolveId(options.guild)!; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const duration = options.duration || undefined; + + // If guild does not exist create it so the modlog can reference a guild. + await Guild.findOrCreate({ + where: { + id: guild + }, + defaults: { + id: guild + } + }); + + const modLogEntry = ModLog.build({ + type: options.type, + user, + moderator, + reason: options.reason, + duration: duration, + guild, + pseudo: options.pseudo ?? false, + evidence: options.evidence + }); + const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { + await util.handleError('createModLogEntry', e); + return null; + }); + + if (!getCaseNumber) return { log: saveResult, caseNum: null }; + + const caseNum = (await ModLog.findAll({ where: { type: options.type, user: user, guild: guild, hidden: 'false' } })) + ?.length; + return { log: saveResult, caseNum }; + } + + public static async createPunishmentEntry(options: { + type: 'mute' | 'ban' | 'role' | 'block'; + user: BushGuildMemberResolvable; + duration: number | undefined; + guild: BushGuildResolvable; + modlog: string; + extraInfo?: Snowflake; + }): Promise { + const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; + const user = (await util.resolveNonCachedUser(options.user))!.id; + const guild = client.guilds.resolveId(options.guild)!; + const type = this.findTypeEnum(options.type)!; + + const entry = ActivePunishment.build( + options.extraInfo + ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo } + : { user, type, guild, expires, modlog: options.modlog } + ); + return await entry.save().catch(async (e) => { + await util.handleError('createPunishmentEntry', e); + return null; + }); + } + + public static async removePunishmentEntry(options: { + type: 'mute' | 'ban' | 'role' | 'block'; + user: BushGuildMemberResolvable; + guild: BushGuildResolvable; + extraInfo?: Snowflake; + }): Promise { + const user = await util.resolveNonCachedUser(options.user); + const guild = client.guilds.resolveId(options.guild); + const type = this.findTypeEnum(options.type); + + if (!user || !guild) return false; + + let success = true; + + const entries = await ActivePunishment.findAll({ + // finding all cases of a certain type incase there were duplicates or something + where: options.extraInfo + ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } + : { user: user.id, guild: guild, type } + }).catch(async (e) => { + await util.handleError('removePunishmentEntry', e); + success = false; + }); + if (entries) { + const promises = entries.map(async (entry) => + entry.destroy().catch(async (e) => { + await util.handleError('removePunishmentEntry', e); + success = false; + }) + ); + + await Promise.all(promises); + } + return success; + } + + private static findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { + const typeMap = { + ['mute']: ActivePunishmentType.MUTE, + ['ban']: ActivePunishmentType.BAN, + ['role']: ActivePunishmentType.ROLE, + ['block']: ActivePunishmentType.BLOCK + }; + return typeMap[type]; + } +} diff --git a/src/lib/common/_autoMod.ts b/src/lib/common/_autoMod.ts deleted file mode 100644 index de36e91..0000000 --- a/src/lib/common/_autoMod.ts +++ /dev/null @@ -1,246 +0,0 @@ -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 { - 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; -} diff --git a/src/lib/common/_moderation.ts b/src/lib/common/_moderation.ts deleted file mode 100644 index 29d66fa..0000000 --- a/src/lib/common/_moderation.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Snowflake } from 'discord-api-types'; -import { - ActivePunishment, - ActivePunishmentType, - BushGuildMember, - BushGuildMemberResolvable, - BushGuildResolvable, - Guild, - ModLog, - ModLogType -} from '..'; - -export class Moderation { - /** - * Checks if a moderator can perform a moderation action on another user. - * @param moderator - The person trying to perform the action. - * @param victim - The person getting punished. - * @param type - The type of punishment - used to format the response. - * @param checkModerator - Whether or not to check if the victim is a moderator. - */ - public static async permissionCheck( - moderator: BushGuildMember, - victim: BushGuildMember, - type: 'mute' | 'unmute' | 'warn' | 'kick' | 'ban' | 'unban' | 'add a punishment role to' | 'remove a punishment role from', - checkModerator = true, - force = false - ): Promise { - if (force) return true; - - // If the victim is not in the guild anymore it will be undefined - if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true; - - if (moderator.guild.id !== victim.guild.id) { - throw new Error('moderator and victim not in same guild'); - } - - const isOwner = moderator.guild.ownerId === moderator.id; - if (moderator.id === victim.id && !type.startsWith('un')) { - return `${util.emojis.error} You cannot ${type} yourself.`; - } - if ( - moderator.roles.highest.position <= victim.roles.highest.position && - !isOwner && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; - } - if ( - victim.roles.highest.position >= victim.guild.me!.roles.highest.position && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; - } - if (checkModerator && victim.permissions.has('MANAGE_MESSAGES') && !(type.startsWith('un') && moderator.id === victim.id)) { - if (await moderator.guild.hasFeature('modsCanPunishMods')) { - return true; - } else { - return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; - } - } - return true; - } - - public static async createModLogEntry( - options: { - type: ModLogType; - user: BushGuildMemberResolvable; - moderator: BushGuildMemberResolvable; - reason: string | undefined | null; - duration?: number; - guild: BushGuildResolvable; - pseudo?: boolean; - evidence?: string; - }, - getCaseNumber = false - ): Promise<{ log: ModLog | null; caseNum: number | null }> { - const user = (await util.resolveNonCachedUser(options.user))!.id; - const moderator = (await util.resolveNonCachedUser(options.moderator))!.id; - const guild = client.guilds.resolveId(options.guild)!; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const duration = options.duration || undefined; - - // If guild does not exist create it so the modlog can reference a guild. - await Guild.findOrCreate({ - where: { - id: guild - }, - defaults: { - id: guild - } - }); - - const modLogEntry = ModLog.build({ - type: options.type, - user, - moderator, - reason: options.reason, - duration: duration, - guild, - pseudo: options.pseudo ?? false, - evidence: options.evidence - }); - const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { - await util.handleError('createModLogEntry', e); - return null; - }); - - if (!getCaseNumber) return { log: saveResult, caseNum: null }; - - const caseNum = (await ModLog.findAll({ where: { type: options.type, user: user, guild: guild, hidden: 'false' } })) - ?.length; - return { log: saveResult, caseNum }; - } - - public static async createPunishmentEntry(options: { - type: 'mute' | 'ban' | 'role' | 'block'; - user: BushGuildMemberResolvable; - duration: number | undefined; - guild: BushGuildResolvable; - modlog: string; - extraInfo?: Snowflake; - }): Promise { - const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; - const user = (await util.resolveNonCachedUser(options.user))!.id; - const guild = client.guilds.resolveId(options.guild)!; - const type = this.findTypeEnum(options.type)!; - - const entry = ActivePunishment.build( - options.extraInfo - ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo } - : { user, type, guild, expires, modlog: options.modlog } - ); - return await entry.save().catch(async (e) => { - await util.handleError('createPunishmentEntry', e); - return null; - }); - } - - public static async removePunishmentEntry(options: { - type: 'mute' | 'ban' | 'role' | 'block'; - user: BushGuildMemberResolvable; - guild: BushGuildResolvable; - extraInfo?: Snowflake; - }): Promise { - const user = await util.resolveNonCachedUser(options.user); - const guild = client.guilds.resolveId(options.guild); - const type = this.findTypeEnum(options.type); - - if (!user || !guild) return false; - - let success = true; - - const entries = await ActivePunishment.findAll({ - // finding all cases of a certain type incase there were duplicates or something - where: options.extraInfo - ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } - : { user: user.id, guild: guild, type } - }).catch(async (e) => { - await util.handleError('removePunishmentEntry', e); - success = false; - }); - if (entries) { - const promises = entries.map(async (entry) => - entry.destroy().catch(async (e) => { - await util.handleError('removePunishmentEntry', e); - success = false; - }) - ); - - await Promise.all(promises); - } - return success; - } - - private static findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { - const typeMap = { - ['mute']: ActivePunishmentType.MUTE, - ['ban']: ActivePunishmentType.BAN, - ['role']: ActivePunishmentType.ROLE, - ['block']: ActivePunishmentType.BLOCK - }; - return typeMap[type]; - } -} diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index a7eabde..51c2795 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -1,6 +1,6 @@ import { Guild, MessageOptions, UserResolvable } from 'discord.js'; import { RawGuildData } from 'discord.js/typings/rawDataTypes'; -import { Moderation } from '../../common/_moderation'; +import { Moderation } from '../../common/Moderation'; import { Guild as GuildDB, GuildFeatures, GuildLogType, GuildModel } from '../../models/Guild'; import { ModLogType } from '../../models/ModLog'; import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient'; diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 051a897..34054c8 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -1,6 +1,6 @@ import { GuildMember, MessageEmbed, Partialize, Role } from 'discord.js'; import { RawGuildMemberData } from 'discord.js/typings/rawDataTypes'; -import { Moderation } from '../../common/_moderation'; +import { Moderation } from '../../common/Moderation'; import { ModLogType } from '../../models/ModLog'; import { BushClient } from '../discord-akairo/BushClient'; import { BushGuild } from './BushGuild'; -- cgit