From a69b4fd4c0f08787ca2490256698e444d687faa3 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Tue, 13 Jan 2026 19:24:19 -0500 Subject: [PATCH 1/5] Add automod event logging to user mod threads - 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 --- app/discord/automod.ts | 102 +++++++++++++++- app/discord/client.server.ts | 1 + app/helpers/escalate.tsx | 7 +- app/helpers/modLog.ts | 164 ++++++++++++++++++++++++++ app/models/reportedMessages.server.ts | 1 + notes/2026-01-13_1_automod-logging.md | 45 +++++++ 6 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 notes/2026-01-13_1_automod-logging.md diff --git a/app/discord/automod.ts b/app/discord/automod.ts index 211b854e..bf6ac86b 100644 --- a/app/discord/automod.ts +++ b/app/discord/automod.ts @@ -1,9 +1,15 @@ -import { Events, type Client } from "discord.js"; +import { + AutoModerationActionType, + Events, + type AutoModerationActionExecution, + type Client, +} from "discord.js"; import { isStaff } from "#~/helpers/discord"; import { isSpam } from "#~/helpers/isSpam"; import { featureStats } from "#~/helpers/metrics"; -import { reportUser } from "#~/helpers/modLog"; +import { reportAutomod, reportUser } from "#~/helpers/modLog"; +import { log } from "#~/helpers/observability"; import { markMessageAsDeleted, ReportReasons, @@ -13,7 +19,99 @@ import { client } from "./client.server"; const AUTO_SPAM_THRESHOLD = 3; +async function handleAutomodAction(execution: AutoModerationActionExecution) { + const { + guild, + userId, + channelId, + messageId, + content, + action, + matchedContent, + matchedKeyword, + autoModerationRule, + } = execution; + + // Only log actions that actually affected a message (BlockMessage, SendAlertMessage) + // Skip Timeout actions as they don't have associated message content + if (action.type === AutoModerationActionType.Timeout) { + log("debug", "Automod", "Skipping timeout action (no message to log)", { + userId, + guildId: guild.id, + ruleId: autoModerationRule?.name, + }); + return; + } + + log("info", "Automod", "Automod action executed", { + userId, + guildId: guild.id, + channelId, + messageId, + actionType: action.type, + ruleName: autoModerationRule?.name, + matchedKeyword, + }); + + // Try to fetch the message if we have a messageId + // The message may have been deleted by automod before we can fetch it + if (messageId && channelId) { + try { + const channel = await guild.channels.fetch(channelId); + if (channel?.isTextBased() && "messages" in channel) { + const message = await channel.messages.fetch(messageId); + // We have the full message, use reportUser + await reportUser({ + reason: ReportReasons.automod, + message, + staff: client.user ?? false, + extra: `Rule: ${autoModerationRule?.name ?? "Unknown"}\nMatched: ${matchedKeyword ?? matchedContent ?? "Unknown"}`, + }); + return; + } + } catch (e) { + log( + "debug", + "Automod", + "Could not fetch message, using fallback logging", + { + messageId, + error: e instanceof Error ? e.message : String(e), + }, + ); + } + } + + // Fallback: message was blocked/deleted or we couldn't fetch it + // Use reportAutomod which doesn't require a Message object + const user = await guild.client.users.fetch(userId); + await reportAutomod({ + guild, + user, + content: content ?? matchedContent ?? "[Content not available]", + channelId: channelId ?? undefined, + messageId: messageId ?? undefined, + ruleName: autoModerationRule?.name ?? "Unknown rule", + matchedKeyword: matchedKeyword ?? matchedContent ?? undefined, + actionType: action.type, + }); +} + export default async (bot: Client) => { + // Handle Discord's built-in automod actions + bot.on(Events.AutoModerationActionExecution, async (execution) => { + try { + await handleAutomodAction(execution); + } catch (e) { + log("error", "Automod", "Failed to handle automod action", { + error: e instanceof Error ? e.message : String(e), + userId: execution.userId, + guildId: execution.guild.id, + }); + } + }); + + // Handle our custom spam detection bot.on(Events.MessageCreate, async (msg) => { if (msg.author.id === bot.user?.id || !msg.guild) return; diff --git a/app/discord/client.server.ts b/app/discord/client.server.ts index 5f19ceb6..ffa98be0 100644 --- a/app/discord/client.server.ts +++ b/app/discord/client.server.ts @@ -13,6 +13,7 @@ export const client = new Client({ GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.DirectMessages, GatewayIntentBits.DirectMessageReactions, + GatewayIntentBits.AutoModerationExecution, ], partials: [Partials.Message, Partials.Channel, Partials.Reaction], }); diff --git a/app/helpers/escalate.tsx b/app/helpers/escalate.tsx index d6f9e5a6..dcd31c9b 100644 --- a/app/helpers/escalate.tsx +++ b/app/helpers/escalate.tsx @@ -7,10 +7,13 @@ import { } from "discord.js"; export async function escalationControls( - reportedMessage: Message, + reportedMessageOrUserId: Message | string, thread: ThreadChannel, ) { - const reportedUserId = reportedMessage.author.id; + const reportedUserId = + typeof reportedMessageOrUserId === "string" + ? reportedMessageOrUserId + : reportedMessageOrUserId.author.id; await thread.send({ content: "Moderator controls", diff --git a/app/helpers/modLog.ts b/app/helpers/modLog.ts index 98bfdb62..a2c92765 100644 --- a/app/helpers/modLog.ts +++ b/app/helpers/modLog.ts @@ -1,10 +1,12 @@ import { formatDistanceToNowStrict } from "date-fns"; import { + AutoModerationActionType, ChannelType, messageLink, MessageReferenceType, type AnyThreadChannel, type APIEmbed, + type Guild, type Message, type MessageCreateOptions, type TextChannel, @@ -45,6 +47,7 @@ const ReadableReasons: Record = { [ReportReasons.track]: "tracked", [ReportReasons.modResolution]: "Mod vote resolved", [ReportReasons.spam]: "detected as spam", + [ReportReasons.automod]: "detected by automod", }; const isForwardedMessage = (message: Message): boolean => { @@ -118,6 +121,167 @@ const getOrCreateUserThread = async (message: Message, user: User) => { return thread; }; +export interface AutomodReport { + guild: Guild; + user: User; + content: string; + channelId?: string; + messageId?: string; + ruleName: string; + matchedKeyword?: string; + actionType: AutoModerationActionType; +} + +const ActionTypeLabels: Record = { + [AutoModerationActionType.BlockMessage]: "blocked message", + [AutoModerationActionType.SendAlertMessage]: "sent alert", + [AutoModerationActionType.Timeout]: "timed out user", + [AutoModerationActionType.BlockMemberInteraction]: "blocked interaction", +}; + +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; +}; + +/** + * Reports an automod action when we don't have a full Message object. + * Used when Discord's automod blocks/deletes a message before we can fetch it. + */ +export const reportAutomod = async ({ + guild, + user, + content, + channelId, + messageId, + ruleName, + matchedKeyword, + actionType, +}: AutomodReport): Promise => { + log("info", "reportAutomod", `Automod triggered for ${user.username}`, { + userId: user.id, + guildId: guild.id, + ruleName, + actionType, + }); + + // Get or create persistent user thread + const thread = await getOrCreateUserThreadForAutomod(guild, user); + + // Get mod log for forwarding + const { modLog, moderator } = await fetchSettings(guild.id, [ + SETTINGS.modLog, + SETTINGS.moderator, + ]); + + // Construct the log message + const channelMention = channelId ? `<#${channelId}>` : "Unknown channel"; + const actionLabel = ActionTypeLabels[actionType] ?? "took action"; + + const logContent = truncateMessage(`**Automod ${actionLabel}** +<@${user.id}> (${user.username}) in ${channelMention} +-# Rule: ${ruleName}${matchedKeyword ? ` · Matched: \`${matchedKeyword}\`` : ""}`).trim(); + + // Send log to thread + const [logMessage] = await Promise.all([ + thread.send({ + content: logContent, + allowedMentions: { roles: moderator ? [moderator] : [] }, + }), + thread.send({ + content: quoteAndEscape(content).trim(), + allowedMentions: {}, + }), + ]); + + // Record to database if we have a messageId + if (messageId) { + await retry(3, async () => { + const result = await recordReport({ + reportedMessageId: messageId, + reportedChannelId: channelId ?? "unknown", + reportedUserId: user.id, + guildId: guild.id, + logMessageId: logMessage.id, + logChannelId: thread.id, + reason: ReportReasons.automod, + extra: `Rule: ${ruleName}`, + }); + + if (!result.wasInserted) { + log( + "warn", + "reportAutomod", + "duplicate detected at database level, retrying check", + ); + throw new Error("Race condition detected in recordReport, retrying…"); + } + + return result; + }); + } + + // Forward to mod log + await logMessage.forward(modLog).catch((e) => { + log("error", "reportAutomod", "failed to forward to modLog", { error: e }); + }); + + // Send summary to parent channel + if (thread.parent?.isSendable()) { + const singleLine = content.slice(0, 80).replaceAll("\n", "\\n "); + const truncatedContent = + singleLine.length > 80 ? `${singleLine.slice(0, 80)}…` : singleLine; + + await thread.parent + .send({ + allowedMentions: {}, + content: `> ${escapeDisruptiveContent(truncatedContent)}\n-# [Automod: ${ruleName}](${messageLink(logMessage.channelId, logMessage.id)})`, + }) + .catch((e) => { + log("error", "reportAutomod", "failed to send summary to parent", { + error: e, + }); + }); + } +}; + // const warningMessages = new (); export const reportUser = async ({ reason, diff --git a/app/models/reportedMessages.server.ts b/app/models/reportedMessages.server.ts index f9de3810..47953805 100644 --- a/app/models/reportedMessages.server.ts +++ b/app/models/reportedMessages.server.ts @@ -18,6 +18,7 @@ export const enum ReportReasons { track = "track", modResolution = "modResolution", spam = "spam", + automod = "automod", } export async function recordReport(data: { diff --git a/notes/2026-01-13_1_automod-logging.md b/notes/2026-01-13_1_automod-logging.md new file mode 100644 index 00000000..1f49cb59 --- /dev/null +++ b/notes/2026-01-13_1_automod-logging.md @@ -0,0 +1,45 @@ +# Automod Event Logging + +Added functionality to log Discord's built-in automod trigger events to user mod threads. + +## Changes + +### Client Intent + +Added `GatewayIntentBits.AutoModerationExecution` to `client.server.ts` to receive automod events. + +### Event Handler (`automod.ts`) + +- Added handler for `Events.AutoModerationActionExecution` +- Skips `Timeout` actions (no message content to log) +- Tries to fetch the message if `messageId` exists + - If successful: uses existing `reportUser()` with `ReportReasons.automod` + - If failed (message blocked/deleted): uses new `reportAutomod()` fallback + +### New Function (`modLog.ts`) + +Created `reportAutomod()` for cases where we don't have a full `Message` object: + +- Gets/creates user thread (reusing pattern from `getOrCreateUserThread`) +- Logs automod-specific info: rule name, matched keyword, action type +- Records to database if `messageId` available +- Forwards to mod log and sends summary to parent channel + +Also modified `escalationControls()` in `escalate.tsx` to accept either a `Message` or just a `userId` string. + +## Design Decisions + +1. **Two-path approach**: Try to fetch the message first for full context, fallback to minimal logging if unavailable. This maximizes information captured. + +2. **Skip Timeout actions**: These don't have associated message content worth logging. The timeout itself is visible in Discord's audit log. + +3. **No MESSAGE_CONTENT intent**: The `content` field in automod events requires privileged intent. We work with what's available (`matchedContent`, `matchedKeyword`). + +4. **Database recording conditional on messageId**: If automod blocked the message before sending, there's no message ID to record. We still log to the thread for visibility. + +## Related Files + +- `app/discord/client.server.ts` - intent added +- `app/discord/automod.ts` - event handler +- `app/helpers/modLog.ts` - `reportAutomod()` function +- `app/helpers/escalate.tsx` - signature update From c5c76fd380278f7fb7e3cc5f76abfdbeb88de3a1 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Wed, 14 Jan 2026 13:47:22 -0500 Subject: [PATCH 2/5] Log all inbound Discord events in dev --- app/discord/automod.ts | 6 +++--- app/discord/gateway.ts | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/discord/automod.ts b/app/discord/automod.ts index bf6ac86b..f06fb004 100644 --- a/app/discord/automod.ts +++ b/app/discord/automod.ts @@ -32,8 +32,7 @@ async function handleAutomodAction(execution: AutoModerationActionExecution) { autoModerationRule, } = execution; - // Only log actions that actually affected a message (BlockMessage, SendAlertMessage) - // Skip Timeout actions as they don't have associated message content + // Only log actions that actually affected a message if (action.type === AutoModerationActionType.Timeout) { log("debug", "Automod", "Skipping timeout action (no message to log)", { userId, @@ -101,10 +100,11 @@ export default async (bot: Client) => { // Handle Discord's built-in automod actions bot.on(Events.AutoModerationActionExecution, async (execution) => { try { + log("info", "automod.logging", "handling automod event", { execution }); await handleAutomodAction(execution); } catch (e) { log("error", "Automod", "Failed to handle automod action", { - error: e instanceof Error ? e.message : String(e), + error: e, userId: execution.userId, guildId: execution.guild.id, }); diff --git a/app/discord/gateway.ts b/app/discord/gateway.ts index 1e9aff98..3c853e5c 100644 --- a/app/discord/gateway.ts +++ b/app/discord/gateway.ts @@ -25,16 +25,28 @@ export default function init() { "info", "Gateway", "Gateway already initialized, skipping duplicate init", - {}, ); return; } - log("info", "Gateway", "Initializing Discord gateway", {}); + log("info", "Gateway", "Initializing Discord gateway"); globalThis.__discordGatewayInitialized = true; void login(); + // Diagnostic: log all raw gateway events + client.on( + Events.Raw, + (packet: { t?: string; op?: number; d?: Record }) => { + log("debug", "Gateway.Raw", packet.t ?? "unknown", { + op: packet.op, + guildId: packet.d?.guild_id, + channelId: packet.d?.channel_id, + userId: packet.d?.user_id, + }); + }, + ); + client.on(Events.ClientReady, async () => { await trackPerformance( "gateway_startup", From d1af56f731814b66b47225c23d2ee36dfefc27ac Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Wed, 14 Jan 2026 13:57:57 -0500 Subject: [PATCH 3/5] Simplify codepaths for automod logs This whole thing tbh should get stripped down into a fresh set of base building blocks, the Discord.js SDK objects aren't working well as abstractions here. It needs to be more testable so we can verify its behavior more precisely --- app/discord/automod.ts | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/app/discord/automod.ts b/app/discord/automod.ts index f06fb004..f2bc6560 100644 --- a/app/discord/automod.ts +++ b/app/discord/automod.ts @@ -52,35 +52,6 @@ async function handleAutomodAction(execution: AutoModerationActionExecution) { matchedKeyword, }); - // Try to fetch the message if we have a messageId - // The message may have been deleted by automod before we can fetch it - if (messageId && channelId) { - try { - const channel = await guild.channels.fetch(channelId); - if (channel?.isTextBased() && "messages" in channel) { - const message = await channel.messages.fetch(messageId); - // We have the full message, use reportUser - await reportUser({ - reason: ReportReasons.automod, - message, - staff: client.user ?? false, - extra: `Rule: ${autoModerationRule?.name ?? "Unknown"}\nMatched: ${matchedKeyword ?? matchedContent ?? "Unknown"}`, - }); - return; - } - } catch (e) { - log( - "debug", - "Automod", - "Could not fetch message, using fallback logging", - { - messageId, - error: e instanceof Error ? e.message : String(e), - }, - ); - } - } - // Fallback: message was blocked/deleted or we couldn't fetch it // Use reportAutomod which doesn't require a Message object const user = await guild.client.users.fetch(userId); From a6b7e098f080a20949e39543a155975b054d08e8 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Thu, 15 Jan 2026 13:07:44 -0500 Subject: [PATCH 4/5] Remove duplicative code --- app/helpers/escalate.tsx | 8 +---- app/helpers/modLog.ts | 71 ++++++++-------------------------------- 2 files changed, 14 insertions(+), 65 deletions(-) diff --git a/app/helpers/escalate.tsx b/app/helpers/escalate.tsx index dcd31c9b..2ae542a0 100644 --- a/app/helpers/escalate.tsx +++ b/app/helpers/escalate.tsx @@ -2,19 +2,13 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, - type Message, type ThreadChannel, } from "discord.js"; export async function escalationControls( - reportedMessageOrUserId: Message | string, + reportedUserId: string, thread: ThreadChannel, ) { - const reportedUserId = - typeof reportedMessageOrUserId === "string" - ? reportedMessageOrUserId - : reportedMessageOrUserId.author.id; - await thread.send({ content: "Moderator controls", components: [ diff --git a/app/helpers/modLog.ts b/app/helpers/modLog.ts index a2c92765..ee19e303 100644 --- a/app/helpers/modLog.ts +++ b/app/helpers/modLog.ts @@ -76,8 +76,7 @@ const makeUserThread = (channel: TextChannel, user: User) => { }); }; -const getOrCreateUserThread = async (message: Message, user: User) => { - const { guild } = message; +const getOrCreateUserThread = async (guild: Guild, user: User) => { if (!guild) throw new Error("Message has no guild"); // Check if we already have a thread for this user @@ -109,7 +108,7 @@ const getOrCreateUserThread = async (message: Message, user: User) => { // Create freestanding private thread const thread = await makeUserThread(modLog, user); - await escalationControls(message, thread); + await escalationControls(user.id, thread); // Store or update the thread reference if (existingThread) { @@ -139,48 +138,6 @@ const ActionTypeLabels: Record = { [AutoModerationActionType.BlockMemberInteraction]: "blocked interaction", }; -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; -}; - /** * Reports an automod action when we don't have a full Message object. * Used when Discord's automod blocks/deletes a message before we can fetch it. @@ -203,7 +160,7 @@ export const reportAutomod = async ({ }); // Get or create persistent user thread - const thread = await getOrCreateUserThreadForAutomod(guild, user); + const thread = await getOrCreateUserThread(guild, user); // Get mod log for forwarding const { modLog, moderator } = await fetchSettings(guild.id, [ @@ -291,13 +248,14 @@ export const reportUser = async ({ }: Omit): Promise< Reported & { allReportedMessages: Report[] } > => { - const { guild } = message; + const { guild, author } = message; if (!guild) throw new Error("Tried to report a message without a guild"); // Check if this exact message has already been reported - const existingReports = await getReportsForMessage(message.id, guild.id); - - const { modLog } = await fetchSettings(guild.id, [SETTINGS.modLog]); + const [existingReports, { modLog }] = await Promise.all([ + getReportsForMessage(message.id, guild.id), + fetchSettings(guild.id, [SETTINGS.modLog]), + ]); const alreadyReported = existingReports.find( (r) => r.reported_message_id === message.id, ); @@ -305,11 +263,11 @@ export const reportUser = async ({ log( "info", "reportUser", - `${message.author.username}, ${reason}. ${alreadyReported ? "already reported" : "new report"}.`, + `${author.username}, ${reason}. ${alreadyReported ? "already reported" : "new report"}.`, ); // Get or create persistent user thread first - const thread = await getOrCreateUserThread(message, message.author); + const thread = await getOrCreateUserThread(guild, author); if (alreadyReported && reason !== ReportReasons.modResolution) { // Message already reported with this reason, just add to thread @@ -338,7 +296,7 @@ export const reportUser = async ({ await recordReport({ reportedMessageId: message.id, reportedChannelId: message.channel.id, - reportedUserId: message.author.id, + reportedUserId: author.id, guildId: guild.id, logMessageId: latestReport.id, logChannelId: thread.id, @@ -363,10 +321,7 @@ export const reportUser = async ({ log("info", "reportUser", "new message reported"); // Get user stats for constructing the log - const previousWarnings = await getUserReportStats( - message.author.id, - guild.id, - ); + const previousWarnings = await getUserReportStats(author.id, guild.id); // Send detailed report info to the user thread const logBody = await constructLog({ @@ -390,7 +345,7 @@ export const reportUser = async ({ const result = await recordReport({ reportedMessageId: message.id, reportedChannelId: message.channel.id, - reportedUserId: message.author.id, + reportedUserId: author.id, guildId: guild.id, logMessageId: logMessage.id, logChannelId: thread.id, From 66cf8551f26f8b0a298a527725fb44f72c876f93 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Thu, 15 Jan 2026 14:56:54 -0500 Subject: [PATCH 5/5] Improve text content of logs --- app/helpers/modLog.ts | 39 +++++++-------------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/app/helpers/modLog.ts b/app/helpers/modLog.ts index ee19e303..6a1fbce4 100644 --- a/app/helpers/modLog.ts +++ b/app/helpers/modLog.ts @@ -145,7 +145,6 @@ const ActionTypeLabels: Record = { export const reportAutomod = async ({ guild, user, - content, channelId, messageId, ruleName, @@ -172,21 +171,15 @@ export const reportAutomod = async ({ const channelMention = channelId ? `<#${channelId}>` : "Unknown channel"; const actionLabel = ActionTypeLabels[actionType] ?? "took action"; - const logContent = truncateMessage(`**Automod ${actionLabel}** -<@${user.id}> (${user.username}) in ${channelMention} --# Rule: ${ruleName}${matchedKeyword ? ` · Matched: \`${matchedKeyword}\`` : ""}`).trim(); + const logContent = + truncateMessage(`<@${user.id}> (${user.username}) triggered automod ${matchedKeyword ? `with text \`${matchedKeyword}\` ` : ""}in ${channelMention} +-# ${ruleName} · Automod ${actionLabel}`).trim(); // Send log to thread - const [logMessage] = await Promise.all([ - thread.send({ - content: logContent, - allowedMentions: { roles: moderator ? [moderator] : [] }, - }), - thread.send({ - content: quoteAndEscape(content).trim(), - allowedMentions: {}, - }), - ]); + const logMessage = await thread.send({ + content: logContent, + allowedMentions: { roles: [moderator] }, + }); // Record to database if we have a messageId if (messageId) { @@ -219,24 +212,6 @@ export const reportAutomod = async ({ await logMessage.forward(modLog).catch((e) => { log("error", "reportAutomod", "failed to forward to modLog", { error: e }); }); - - // Send summary to parent channel - if (thread.parent?.isSendable()) { - const singleLine = content.slice(0, 80).replaceAll("\n", "\\n "); - const truncatedContent = - singleLine.length > 80 ? `${singleLine.slice(0, 80)}…` : singleLine; - - await thread.parent - .send({ - allowedMentions: {}, - content: `> ${escapeDisruptiveContent(truncatedContent)}\n-# [Automod: ${ruleName}](${messageLink(logMessage.channelId, logMessage.id)})`, - }) - .catch((e) => { - log("error", "reportAutomod", "failed to send summary to parent", { - error: e, - }); - }); - } }; // const warningMessages = new ();