Skip to content

Conversation

@vcarl
Copy link
Member

@vcarl vcarl commented Jan 14, 2026

  • Add AutoModerationExecution intent to Discord client
  • Handle AutoModerationActionExecution events in automod.ts
  • Create reportAutomod() function for cases where message isn't available
  • Modify escalationControls() to accept userId string directly
  • Add automod enum value to ReportReasons

When automod triggers, the bot now logs the action to the user's mod thread. If the message is still available, it uses the full reportUser() flow. If blocked/deleted by automod, it uses a fallback that logs available context (rule name, matched content, action type).

- Add AutoModerationExecution intent to Discord client
- Handle AutoModerationActionExecution events in automod.ts
- Create reportAutomod() function for cases where message isn't available
- Modify escalationControls() to accept userId string directly
- Add automod enum value to ReportReasons

When automod triggers, the bot now logs the action to the user's mod
thread. If the message is still available, it uses the full reportUser()
flow. If blocked/deleted by automod, it uses a fallback that logs
available context (rule name, matched content, action type).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vcarl vcarl requested a review from Copilot January 14, 2026 00:25
@what-the-diff
Copy link

what-the-diff bot commented Jan 14, 2026

PR Summary

  • Logging for Discord Built-in Automoderation Events
    The team has added a new feature to keep a record of built-in automoderation events on Discord to user moderation threads.

  • Automod Event Handling
    An event handler has been set up to help manage automoderation actions. However, for 'Timeout' actions, no logs are being kept as these events generally don't include any message content.

  • Message Fetch and Report
    The PR includes some improvements in how we handle messages. When a 'messageId' is available, the system tries to fetch the message. If this is successful, the system uses 'reportUser()'. If it fails, the system falls back on a 'reportAutomod()' function.

  • New 'reportAutomod()' Function
    A new feature of logging is introduced in case we do not have a full 'Message' object available.

  • Escalation Controls
    The escalation controls have been updated. Now they can accept a 'Message' or a 'userId' string.

  • ReportReasons Update
    The 'ReportReasons' category has been updated to include 'automod'.

  • New Documentation
    The team has created new documentation explaining the changes and the thought process behind these changes.

@vcarl
Copy link
Member Author

vcarl commented Jan 14, 2026

I still need to properly review and vet this, there are probably a couple of significant improvements left to be made

@vcarl vcarl linked an issue Jan 14, 2026 that may be closed by this pull request
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds logging functionality for Discord's built-in automod trigger events to user moderation threads.

Changes:

  • Added AutoModerationExecution gateway intent to receive automod events
  • Implemented event handler for automod actions with two-path approach: full message logging when available, fallback logging otherwise
  • Created reportAutomod() function for logging automod actions without full message objects
  • Modified escalationControls() to accept either Message or userId for broader compatibility
  • Added automod enum value to ReportReasons

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
notes/2026-01-13_1_automod-logging.md Documentation of implementation approach and design decisions
app/models/reportedMessages.server.ts Added automod reason enum value
app/helpers/modLog.ts Implemented reportAutomod() function and helper for thread creation
app/helpers/escalate.tsx Made escalationControls() accept Message or userId string
app/discord/client.server.ts Added AutoModerationExecution intent
app/discord/automod.ts Implemented AutoModerationActionExecution event handler

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


// Send summary to parent channel
if (thread.parent?.isSendable()) {
const singleLine = content.slice(0, 80).replaceAll("\n", "\\n ");
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The content slicing logic is flawed. Line 268 already slices the content to 80 characters with content.slice(0, 80), but then line 270 checks if singleLine.length > 80 and slices again to 80 characters. Since singleLine is already at most 80 characters, the second slice is redundant and the condition will never be true. The check should be against the original content.length instead, or the logic should be restructured.

Suggested change
const singleLine = content.slice(0, 80).replaceAll("\n", "\\n ");
const singleLine = content.replaceAll("\n", "\\n ");

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +182
const getOrCreateUserThreadForAutomod = async (guild: Guild, user: User) => {
// Check if we already have a thread for this user
const existingThread = await getUserThread(user.id, guild.id);

if (existingThread) {
try {
// Verify the thread still exists and is accessible
const thread = await guild.channels.fetch(existingThread.thread_id);
if (thread?.isThread()) {
return thread;
}
} catch (error) {
log(
"warn",
"getOrCreateUserThreadForAutomod",
"Existing thread not accessible, will create new one",
{ error },
);
}
}

// Create new thread and store in database
const { modLog: modLogId } = await fetchSettings(guild.id, [SETTINGS.modLog]);
const modLog = await guild.channels.fetch(modLogId);
if (!modLog || modLog.type !== ChannelType.GuildText) {
throw new Error("Invalid mod log channel");
}

// Create freestanding private thread
const thread = await makeUserThread(modLog, user);
await escalationControls(user.id, thread);

// Store or update the thread reference
if (existingThread) {
await updateUserThread(user.id, guild.id, thread.id);
} else {
await createUserThread(user.id, guild.id, thread.id);
}

return thread;
};
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is significant code duplication between getOrCreateUserThread and getOrCreateUserThreadForAutomod. The only difference is the call to escalationControls passing different arguments (message vs user.id). Consider refactoring by extracting the common logic into a shared helper function or modifying getOrCreateUserThread to accept optional parameters to handle both cases.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +36
// Only log actions that actually affected a message (BlockMessage, SendAlertMessage)
// Skip Timeout actions as they don't have associated message content
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "Only log actions that actually affected a message (BlockMessage, SendAlertMessage)" but the code only skips Timeout actions. BlockMemberInteraction actions will also be logged even though they may not have associated message content. Consider whether BlockMemberInteraction actions should also be skipped, or update the comment to accurately reflect what actions are being logged.

Suggested change
// Only log actions that actually affected a message (BlockMessage, SendAlertMessage)
// Skip Timeout actions as they don't have associated message content
// Skip logging Timeout actions as they don't have associated message content.
// All other automod actions are logged, even if they may not directly involve a message.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +93
channelId: channelId ?? undefined,
messageId: messageId ?? undefined,
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression channelId ?? undefined is redundant. If channelId is already null or undefined, the nullish coalescing operator will return undefined, but if channelId is a string (including an empty string), it will return that string. This could be simplified to just channelId since the type already allows string | undefined. The same applies to messageId ?? undefined on line 93.

Suggested change
channelId: channelId ?? undefined,
messageId: messageId ?? undefined,
channelId,
messageId,

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +220
const logContent = truncateMessage(`**Automod ${actionLabel}**
<@${user.id}> (${user.username}) in ${channelMention}
-# Rule: ${ruleName}${matchedKeyword ? ` · Matched: \`${matchedKeyword}\`` : ""}`).trim();
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If matchedKeyword contains a backtick character, it will break the inline code formatting. Consider escaping backticks in the matchedKeyword before embedding it in the template literal, or use a different delimiter that doesn't conflict with Discord markdown.

Suggested change
const logContent = truncateMessage(`**Automod ${actionLabel}**
<@${user.id}> (${user.username}) in ${channelMention}
-# Rule: ${ruleName}${matchedKeyword ? ` · Matched: \`${matchedKeyword}\`` : ""}`).trim();
const safeMatchedKeyword =
matchedKeyword != null ? matchedKeyword.replace(/`/g, "\\`") : matchedKeyword;
const logContent = truncateMessage(`**Automod ${actionLabel}**
<@${user.id}> (${user.username}) in ${channelMention}
-# Rule: ${ruleName}${safeMatchedKeyword ? ` · Matched: \`${safeMatchedKeyword}\`` : ""}`).trim();

Copilot uses AI. Check for mistakes.
await thread.parent
.send({
allowedMentions: {},
content: `> ${escapeDisruptiveContent(truncatedContent)}\n-# [Automod: ${ruleName}](${messageLink(logMessage.channelId, logMessage.id)})`,
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ruleName contains square brackets or other markdown characters, it could break the markdown link formatting. Consider escaping special markdown characters in ruleName before using it in the link text.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Record auto mod mentions in moderation threads

2 participants