diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/commands/fun/minesweeper.ts | 2 | ||||
-rw-r--r-- | src/commands/info/help.ts | 2 | ||||
-rw-r--r-- | src/commands/moderation/role.ts | 1 | ||||
-rw-r--r-- | src/lib/common/AutoMod.ts | 175 | ||||
-rw-r--r-- | src/lib/common/ButtonPaginator.ts | 13 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushMessage.ts | 2 | ||||
-rw-r--r-- | src/lib/models/Guild.ts | 4 | ||||
-rw-r--r-- | src/tasks/cpuUsage.ts | 1 | ||||
-rw-r--r-- | src/tasks/removeExpiredPunishements.ts | 1 | ||||
-rw-r--r-- | src/tasks/updateCache.ts | 1 | ||||
-rw-r--r-- | src/tasks/updateStats.ts | 1 | ||||
-rw-r--r-- | src/tasks/updateSuperUsers.ts | 1 |
12 files changed, 184 insertions, 20 deletions
diff --git a/src/commands/fun/minesweeper.ts b/src/commands/fun/minesweeper.ts index 7ef1de7..16352ce 100644 --- a/src/commands/fun/minesweeper.ts +++ b/src/commands/fun/minesweeper.ts @@ -1,5 +1,5 @@ import { BushCommand, type BushMessage, type BushSlashMessage } from '#lib'; -import Minesweeper from '@notenoughupdates/discord.js-minesweeper'; +import { Minesweeper } from '@notenoughupdates/discord.js-minesweeper'; export default class MinesweeperCommand extends BushCommand { public constructor() { diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 8b6720b..455ad5f 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -139,7 +139,7 @@ export default class HelpCommand extends BushCommand { }) ); } - if (packageDotJSON) + if (packageDotJSON?.repository) row.addComponents( new MessageButton({ style: 'LINK', diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts index 7ca0a5d..275db38 100644 --- a/src/commands/moderation/role.ts +++ b/src/commands/moderation/role.ts @@ -105,6 +105,7 @@ export default class RoleCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { action: 'add' | 'remove'; member: BushGuildMember; role: BushRole; duration?: number | null; force?: boolean } ) { + if (!args.role) return await message.util.reply(`${util.emojis.error} You must specify a role.`); if (args.duration === null) args.duration = 0; if ( !message.member!.permissions.has('MANAGE_ROLES') && diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts index 5fd5d2d..c52754a 100644 --- a/src/lib/common/AutoMod.ts +++ b/src/lib/common/AutoMod.ts @@ -4,18 +4,36 @@ import badLinksSecretArray from '../badlinks-secret.js'; // I cannot make this p import badLinksArray from '../badlinks.js'; import badWords from '../badwords.js'; +/** + * Handles auto moderation functionality. + */ export class AutoMod { + /** + * The message to check for blacklisted phrases on + */ private message: BushMessage; + /** + * Whether or not a punishment has already been given to the user + */ + private punished = false; + + /** + * @param message The message to check and potentially perform automod actions to + */ public constructor(message: BushMessage) { this.message = message; if (message.author.id === client.user?.id) return; void this.handle(); } + /** + * Handles the auto moderation + */ private async handle() { if (this.message.channel.type === 'DM' || !this.message.guild) return; - if (!(await this.message.guild.hasFeature('automod'))) return; + const hasFeature = this.message.guild.hasFeature; + if (!(await hasFeature('automod'))) return; const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? {}; const badLinks: BadWords = {}; @@ -34,8 +52,8 @@ export class AutoMod { const result = { ...this.checkWords(customAutomodPhrases), - ...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? {} : badWords), - ...this.checkWords((await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? {} : badLinks) + ...this.checkWords((await hasFeature('excludeDefaultAutomod')) ? {} : badWords), + ...this.checkWords((await hasFeature('excludeAutomodScamLinks')) ? {} : badLinks) }; if (Object.keys(result).length === 0) return; @@ -44,7 +62,7 @@ export class AutoMod { .map(([key, value]) => ({ word: key, ...value })) .sort((a, b) => b.severity - a.severity)[0]; - if (highestOffence.severity === undefined || highestOffence.severity === null) + if (highestOffence.severity === undefined || highestOffence.severity === null) { void this.message.guild.sendLogChannel('error', { embeds: [ { @@ -54,12 +72,19 @@ export class AutoMod { } ] }); - else { + } else { const color = this.punish(highestOffence); void this.log(highestOffence, color, result); } + + if (!this.punished && (await hasFeature('delScamMentions'))) void this.checkScamMentions(); } + /** + * Checks if any of the words provided are in the message + * @param words The words to check for + * @returns The blacklisted words found in the message + */ private checkWords(words: BadWords): BadWords { if (Object.keys(words).length === 0) return {}; @@ -79,17 +104,81 @@ export class AutoMod { return matchedWords; } + /** + * If the message contains '@everyone' or '@here' and it contains a common scam phrase, it will be deleted + * @returns + */ + private async checkScamMentions() { + const includes = this.message.content.toLocaleLowerCase().includes; + if (!includes('@everyone' || !includes('@here'))) return; + // It would be bad if we deleted a message that actually pinged @everyone or @here + if (this.message.member?.permissionsIn(this.message.channelId).has('MENTION_EVERYONE') || this.message.mentions.everyone) + return; + + if ( + includes('steam') || + includes('www.youtube.com') || + includes('youtu.be') || + includes('nitro') || + includes('1 month') || + includes('3 months') || + includes('personalize your profile') || + includes('even more') || + includes('xbox and discord') || + includes('left over') || + includes('check this lol') || + includes('airdrop') + ) { + const color = this.punish({ severity: Severity.TEMP_MUTE, reason: 'everyone mention and scam phrase' } as HighestOffence); + void this.message.guild!.sendLogChannel('automod', { + embeds: [ + new MessageEmbed() + .setTitle(`[Severity ${Severity.TEMP_MUTE}] Mention Scam Deleted`) + .setDescription( + `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From**: <#${this.message.channel.id}> [Jump to context](${this.message.url})` + ) + .addField('Message Content', `${await util.codeblock(this.message.content, 1024)}`) + .setColor(color) + .setTimestamp() + ], + components: + Severity.TEMP_MUTE >= 2 + ? [ + new MessageActionRow().addComponents( + new MessageButton() + .setStyle('DANGER') + .setLabel('Ban User') + .setCustomId(`automod;ban;${this.message.author.id};everyone mention and scam phrase`) + ) + ] + : undefined + }); + } + } + + /** + * Format a string according to the word options + * @param string The string to format + * @param wordOptions The word options to format with + * @returns The formatted string + */ 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 }) { + /** + * Punishes the user based on the severity of the offence + * @param highestOffence The highest offence to punish the user for + * @returns The color of the embed that the log should, based on the severity of the offence + */ + private punish(highestOffence: HighestOffence) { let color; switch (highestOffence.severity) { case Severity.DELETE: { color = util.colors.lightGray; void this.message.delete().catch((e) => deleteError.bind(this, e)); + this.punished = true; break; } case Severity.WARN: { @@ -99,6 +188,7 @@ export class AutoMod { moderator: this.message.guild!.me!, reason: `[AutoMod] ${highestOffence.reason}` }); + this.punished = true; break; } case Severity.TEMP_MUTE: { @@ -109,6 +199,7 @@ export class AutoMod { reason: `[AutoMod] ${highestOffence.reason}`, duration: 900_000 // 15 minutes }); + this.punished = true; break; } case Severity.PERM_MUTE: { @@ -119,6 +210,7 @@ export class AutoMod { reason: `[AutoMod] ${highestOffence.reason}`, duration: 0 // permanent }); + this.punished = true; break; } default: { @@ -142,7 +234,13 @@ export class AutoMod { } } - private async log(highestOffence: BadWordDetails & { word: string }, color: `#${string}`, offences: BadWords) { + /** + * Log an automod infraction to the guild's specified automod log channel + * @param highestOffence The highest severity word found in the message + * @param color The color that the log embed should be (based on the severity) + * @param offences The other offences that were also matched in the message + */ + private async log(highestOffence: HighestOffence, color: `#${string}`, offences: BadWords) { void client.console.info( 'autoMod', `Severity <<${highestOffence.severity}>> action performed on <<${this.message.author.tag}>> (<<${ @@ -150,7 +248,7 @@ export class AutoMod { }>>) in <<#${(this.message.channel as TextChannel).name}>> in <<${this.message.guild!.name}>>` ); - return await this.message.guild!.sendLogChannel('automod', { + await this.message.guild!.sendLogChannel('automod', { embeds: [ new MessageEmbed() .setTitle(`[Severity ${highestOffence.severity}] Automod Action Performed`) @@ -179,6 +277,10 @@ export class AutoMod { }); } + /** + * Handles the ban button in the automod log. + * @param interaction The button interaction. + */ public static async handleInteraction(interaction: BushButtonInteraction) { if (!interaction.memberPermissions?.has('BAN_MEMBERS')) return interaction.reply({ @@ -228,25 +330,74 @@ export class AutoMod { } } +/** + * The severity of the blacklisted word + */ export const enum Severity { - /** Delete message */ + /** + * Delete message + */ DELETE, - /** Delete message and warn user */ + + /** + * Delete message and warn user + */ WARN, - /** Delete message and mute user for 15 minutes */ + + /** + * Delete message and mute user for 15 minutes + */ TEMP_MUTE, - /** Delete message and mute user permanently */ + + /** + * Delete message and mute user permanently + */ PERM_MUTE } +/** + * Details about a blacklisted word + */ interface BadWordDetails { + /** + * The severity of the word + */ severity: Severity; + + /** + * Whether or not to ignore spaces when checking for the word + */ ignoreSpaces: boolean; + + /** + * Whether or not to ignore case when checking for the word + */ ignoreCapitalization: boolean; + + /** + * The reason that this word is blacklisted (used for the punishment reason) + */ reason: string; + + /** + * Whether or not the word is regex + */ regex: boolean; } +interface HighestOffence extends BadWordDetails { + /** + * The word that is blacklisted + */ + word: string; +} + +/** + * Blacklisted words mapped to their details + */ export interface BadWords { + /** + * The blacklisted word + */ [key: string]: BadWordDetails; } diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts index b8ae249..983eb56 100644 --- a/src/lib/common/ButtonPaginator.ts +++ b/src/lib/common/ButtonPaginator.ts @@ -18,11 +18,11 @@ export class ButtonPaginator { /** * Sends multiple embeds with controls to switch between them - * @param message - The message to respond to - * @param embeds - The embeds to switch between - * @param text - The text send with the embeds (optional) - * @param deleteOnExit - Whether to delete the message when the exit button is clicked (defaults to true) - * @param startOn - The page to start from (**not** the index) + * @param message The message to respond to + * @param embeds The embeds to switch between + * @param text The text send with the embeds (optional) + * @param deleteOnExit Whether to delete the message when the exit button is clicked (defaults to true) + * @param startOn The page to start from (**not** the index) */ public static async send( message: BushMessage | BushSlashMessage, @@ -37,6 +37,9 @@ export class ButtonPaginator { return await new ButtonPaginator(message, embeds, text, deleteOnExit, startOn).send(); } + /** + * The number of pages in the paginator + */ protected get numPages(): number { return this.embeds.length; } diff --git a/src/lib/extensions/discord.js/BushMessage.ts b/src/lib/extensions/discord.js/BushMessage.ts index c722f3d..9f6d422 100644 --- a/src/lib/extensions/discord.js/BushMessage.ts +++ b/src/lib/extensions/discord.js/BushMessage.ts @@ -18,7 +18,7 @@ export type PartialBushMessage = Partialize< export class BushMessage<Cached extends boolean = boolean> extends Message<Cached> { public declare readonly client: BushClient; public declare util: BushCommandUtil<BushMessage<true>>; - public declare readonly guild: BushGuild | null; + public declare readonly guild: If<Cached, BushGuild>; public declare readonly member: BushGuildMember | null; public declare author: BushUser; public declare readonly channel: If<Cached, BushGuildTextBasedChannel, BushTextBasedChannels>; diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index 02f487b..50113bf 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -280,6 +280,10 @@ export const guildFeaturesObj = asGuildFeature({ name: 'Exclude Automod Scam Links', description: 'Opt out of having automod delete scam links.' }, + delScamMentions: { + name: 'Delete Scam Mentions', + description: 'Deletes messages with @everyone and @here mentions that have common scam phrases.' + }, autoPublish: { name: 'Auto Publish', description: 'Publishes messages in configured announcement channels.' diff --git a/src/tasks/cpuUsage.ts b/src/tasks/cpuUsage.ts index e597b31..882d660 100644 --- a/src/tasks/cpuUsage.ts +++ b/src/tasks/cpuUsage.ts @@ -8,6 +8,7 @@ export default class CpuUsageTask extends BushTask { runOnStart: true }); } + public override async exec() { const cpu = await osu.cpu.usage(client.stats.cpu === undefined ? 100 : 60_000); client.stats.cpu = cpu; diff --git a/src/tasks/removeExpiredPunishements.ts b/src/tasks/removeExpiredPunishements.ts index 6662292..8197cc5 100644 --- a/src/tasks/removeExpiredPunishements.ts +++ b/src/tasks/removeExpiredPunishements.ts @@ -8,6 +8,7 @@ export default class RemoveExpiredPunishmentsTask extends BushTask { runOnStart: true }); } + public override async exec() { const expiredEntries = await ActivePunishment.findAll({ where: { diff --git a/src/tasks/updateCache.ts b/src/tasks/updateCache.ts index 16683f0..8bf92d5 100644 --- a/src/tasks/updateCache.ts +++ b/src/tasks/updateCache.ts @@ -9,6 +9,7 @@ export default class UpdateCacheTask extends BushTask { runOnStart: false // done in preinit task }); } + public override async exec() { await UpdateCacheTask.updateGlobalCache(client); await UpdateCacheTask.#updateGuildCache(client); diff --git a/src/tasks/updateStats.ts b/src/tasks/updateStats.ts index d6cabaa..8813343 100644 --- a/src/tasks/updateStats.ts +++ b/src/tasks/updateStats.ts @@ -8,6 +8,7 @@ export default class UpdateStatsTask extends BushTask { runOnStart: true }); } + public override async exec() { const row = (await Stat.findByPk(client.config.environment)) ?? (await Stat.create({ environment: client.config.environment })); diff --git a/src/tasks/updateSuperUsers.ts b/src/tasks/updateSuperUsers.ts index ffbf550..ba3e90c 100644 --- a/src/tasks/updateSuperUsers.ts +++ b/src/tasks/updateSuperUsers.ts @@ -8,6 +8,7 @@ export default class UpdateSuperUsersTask extends BushTask { runOnStart: true }); } + public override async exec() { const superUsers = client.guilds.cache .get(client.config.supportGuild.id) |