diff --git a/README.md b/README.md index 011620dd..9e8f6126 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,23 @@ Can be used to repeat the same 'talk' command without typing it out again. --- +## Automatic Features + +### AutoMod Response + +The bot automatically monitors Discord's AutoMod system and responds when messages are blocked for profanity or custom keyword violations. When a user's message is deleted by AutoMod due to banned words, the bot will: + +- Post a lighthearted "Tsk tsk" message mentioning the user +- Display a humorous image + +This feature works automatically with your server's existing AutoMod rules - no configuration needed. It only responds to keyword-based message deletions (profanity filters and custom word lists). + +--- + +### Uptime + +The bot may be [configured](#configure-the-bot) to automatically ping an uptime monitor of your choosing every few minutes, to keep easier track of server downtime. + ## Usage or Development If you've read this far, and don't plan to run or develop on the bot yourself, or are not curious how to do so, you may leave now. This part is quite boring. But feel free to read on anyway! diff --git a/package.json b/package.json index 79985108..a6e628de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "csbot", - "version": "0.17.0", + "version": "0.19.0", "private": true, "description": "The One beneath the Supreme Overlord's rule. A bot to help manage the BYU CS Discord, successor to Ze Kaiser (https://github.com/arkenstorm/ze-kaiser)", "keywords": [ @@ -88,4 +88,4 @@ "engines": { "node": ">=20.6.0" } -} +} \ No newline at end of file diff --git a/src/assets/christian-server.jpg b/src/assets/christian-server.jpg new file mode 100644 index 00000000..5eab1f2a Binary files /dev/null and b/src/assets/christian-server.jpg differ diff --git a/src/events/autoModerationActionExecution.ts b/src/events/autoModerationActionExecution.ts new file mode 100644 index 00000000..af185e33 --- /dev/null +++ b/src/events/autoModerationActionExecution.ts @@ -0,0 +1,61 @@ +import { AttachmentBuilder, AutoModerationActionType, AutoModerationRuleTriggerType } from 'discord.js'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +import { onEvent } from '../helpers/onEvent.js'; +import { debug, error, info } from '../logger.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const assetsDir = path.resolve(__dirname, '..', 'assets'); + +/** + * The event handler for AutoMod action executions + * Sends an image when AutoMod deletes a message due to profanity or custom keywords + */ +export const autoModerationActionExecution = onEvent('autoModerationActionExecution', { + once: false, + async execute(actionExecution) { + try { + // Check if the action was blocking a message (deletion) + const isBlockMessage = actionExecution.action.type === AutoModerationActionType.BlockMessage; + + if (!isBlockMessage) { + // Not a message deletion, ignore + return; + } + + // Check if the trigger was keyword-based (profanity or custom word list) + const isKeywordTrigger = + actionExecution.ruleTriggerType === AutoModerationRuleTriggerType.Keyword || + actionExecution.ruleTriggerType === AutoModerationRuleTriggerType.KeywordPreset; + + if (!isKeywordTrigger) { + // Not a keyword trigger, ignore + debug('AutoMod action was not keyword-based, ignoring'); + return; + } + + // Get the channel where the infraction happened + const channel = actionExecution.channel; + if (!channel?.isTextBased()) { + debug('AutoMod action occurred in non-text channel, ignoring'); + return; + } + + info( + `AutoMod blocked message in ${channel.name} - Rule: ${actionExecution.autoModerationRule?.name ?? 'Unknown'}, Matched: "${actionExecution.matchedKeyword}"` + ); + + // Send a message mentioning the user with the Christian server image + const attachment = new AttachmentBuilder(path.join(assetsDir, 'christian-server.jpg'), { + name: 'christian-server.jpg', + }); + await channel.send({ + content: `Tsk tsk, <@${actionExecution.userId}>`, + files: [attachment], + }); + } catch (error_) { + error('Failed to handle AutoMod action execution:', error_); + } + }, +}); diff --git a/src/events/index.ts b/src/events/index.ts index a2b581e1..9c59367f 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -53,12 +53,14 @@ export function registerEventHandlers(client: Client): void { } // Install event handlers +import { autoModerationActionExecution } from './autoModerationActionExecution.js'; import { error } from './error.js'; import { interactionCreate } from './interactionCreate.js'; import { messageReactionAdd } from './messageReactionAdd.js'; import { messageReactionRemove } from './messageReactionRemove.js'; import { ready } from './ready.js'; +_add(autoModerationActionExecution as EventHandler); _add(error as EventHandler); _add(interactionCreate as EventHandler); _add(messageReactionAdd as EventHandler); diff --git a/src/main.ts b/src/main.ts index 6951f071..224e0f8a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,6 +21,7 @@ export async function _main(): Promise { GatewayIntentBits.GuildMessageTyping, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.MessageContent, + GatewayIntentBits.AutoModerationExecution, ], partials: [Partials.Reaction, Partials.Channel, Partials.Message], allowedMentions: {