From 6cf23b42f789ff9a450cfb0092d6f20b1187fbee Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Fri, 20 Feb 2026 17:05:45 -0500 Subject: [PATCH 1/2] Log mod-deleted messages to both deletion and moderation threads When a moderator deletes a message, it's now logged to both the per-user deletion thread and the moderation thread. This provides better visibility of mod actions in the moderation log alongside kicks, bans, and timeouts. Co-Authored-By: Claude Opus 4.6 --- app/discord/deletionLogger.ts | 60 +++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/app/discord/deletionLogger.ts b/app/discord/deletionLogger.ts index babba536..7c065e7d 100644 --- a/app/discord/deletionLogger.ts +++ b/app/discord/deletionLogger.ts @@ -13,6 +13,7 @@ import { logEffect } from "#~/effects/observability"; import { quoteMessageContent } from "#~/helpers/discord"; import { getOrCreateDeletionLogThread } from "#~/models/deletionLogThreads"; import { fetchSettingsEffect, SETTINGS } from "#~/models/guilds.server"; +import { getOrCreateUserThread } from "#~/models/userThreads"; import { MessageCacheService, @@ -110,8 +111,8 @@ export async function startDeletionLogging(client: Client) { const sent = ``; const header = uncachedAuditEntry?.executor - ? `<@${uncachedAuditEntry.executor.id}> deleted from ${channelMention}, sent ${sent}` - : `Message deleted from ${channelMention}, sent ${sent}`; + ? `-# <@${uncachedAuditEntry.executor.id}> deleted from ${channelMention}, sent ${sent}` + : `-# Message deleted from ${channelMention}, sent ${sent}`; yield* Effect.tryPromise({ try: () => @@ -164,23 +165,23 @@ export async function startDeletionLogging(client: Client) { const sent = ``; const header = auditEntry?.executor - ? `<@${auditEntry.executor.id}> deleted from ${channelMention}, sent ${sent}` - : `Message deleted from ${channelMention}, sent ${sent}`; + ? `-# <@${auditEntry.executor.id}> deleted from ${channelMention}, sent ${sent}` + : `-# Message deleted from ${channelMention}, sent ${sent}`; + + const embed = { + description: [ + header, + `<@${user.id}>`, + quoteMessageContent(content ?? "*(content not cached)*"), + ].join("\n"), + color: Colors.Red, + }; yield* Effect.tryPromise({ try: () => thread.send({ allowedMentions: { parse: [] }, - embeds: [ - { - description: [ - header, - `<@${user.id}>`, - quoteMessageContent(content ?? "*(content not cached)*"), - ].join("\n"), - color: Colors.Red, - }, - ], + embeds: [embed], }), catch: (error) => logEffect( @@ -190,6 +191,37 @@ export async function startDeletionLogging(client: Client) { { guildId: guild.id, error: String(error) }, ), }).pipe(Effect.catchAll((e) => e)); + + // If a mod deleted this message, also log to the moderation thread + if (auditEntry?.executor) { + const modThread = yield* getOrCreateUserThread(guild, user).pipe( + Effect.catchAll((error) => + logEffect( + "warn", + "DeletionLogger", + "Failed to get/create moderation thread for mod deletion", + { guildId: guild.id, userId: user.id, error: String(error) }, + ), + ), + ); + + if (modThread) { + yield* Effect.tryPromise({ + try: () => + modThread.send({ + allowedMentions: { parse: [] }, + embeds: [embed], + }), + catch: (error) => + logEffect( + "warn", + "DeletionLogger", + "Failed to post mod deletion to moderation thread", + { guildId: guild.id, error: String(error) }, + ), + }).pipe(Effect.catchAll((e) => e)); + } + } }).pipe( Effect.catchAll((e) => logEffect( From f569844c7784c4f24307de18b49961ff8f93d793 Mon Sep 17 00:00:00 2001 From: Carl Vitullo Date: Fri, 20 Feb 2026 17:30:35 -0500 Subject: [PATCH 2/2] Fix modreport to include reports of deleted messages The /modreport command was incorrectly filtering out all reports where the reported message had been deleted (deleted_at IS NOT NULL). This meant users who had messages reported and then deleted showed no moderation history. The deleted_at timestamp indicates the reported MESSAGE was deleted (a moderation action), not that the report should be hidden. These are actually the most important reports to show since they represent successful moderation. Removed the deleted_at filter from all modreport-related queries: - getUserReportStats (3 queries) - getSpamReportCount (1 query) - getUserReportSummary (6 queries) - getMonthlyReportCounts (1 query) - getRecentReportCount (2 queries) - getChannelBreakdown (1 query) - getStaffBreakdown (1 query) Co-Authored-By: Claude Opus 4.6 --- app/models/reportedMessages.ts | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/app/models/reportedMessages.ts b/app/models/reportedMessages.ts index 1cf07312..6eddf45d 100644 --- a/app/models/reportedMessages.ts +++ b/app/models/reportedMessages.ts @@ -158,24 +158,21 @@ export const getUserReportStats = (userId: string, guildId: string) => .selectFrom("reported_messages") .select((eb) => eb.fn.count("id").as("count")) .where("reported_user_id", "=", userId) - .where("guild_id", "=", guildId) - .where("deleted_at", "is", null), + .where("guild_id", "=", guildId), kysely .selectFrom("reported_messages") .select(({ fn }) => fn.count("reported_message_id").distinct().as("count"), ) .where("reported_user_id", "=", userId) - .where("guild_id", "=", guildId) - .where("deleted_at", "is", null), + .where("guild_id", "=", guildId), kysely .selectFrom("reported_messages") .select(({ fn }) => fn.count("reported_channel_id").distinct().as("count"), ) .where("reported_user_id", "=", userId) - .where("guild_id", "=", guildId) - .where("deleted_at", "is", null), + .where("guild_id", "=", guildId), ]); return { @@ -200,8 +197,7 @@ export const getSpamReportCount = (userId: string, guildId: string) => .select((eb) => eb.fn.count("id").as("count")) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("reason", "=", ReportReasons.spam) - .where("deleted_at", "is", null); + .where("reason", "=", ReportReasons.spam); return Number(result?.count ?? 0); }).pipe( @@ -233,7 +229,6 @@ export const getUserReportSummary = (userId: string, guildId: string) => .select((eb) => eb.fn.count("id").as("count")) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .groupBy("reason") .orderBy("count", "desc"), // Get first report (earliest created_at) @@ -242,7 +237,6 @@ export const getUserReportSummary = (userId: string, guildId: string) => .select("created_at") .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .orderBy("created_at", "asc") .limit(1), // Get last report (latest created_at) @@ -251,7 +245,6 @@ export const getUserReportSummary = (userId: string, guildId: string) => .select("created_at") .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .orderBy("created_at", "desc") .limit(1), kysely @@ -259,7 +252,6 @@ export const getUserReportSummary = (userId: string, guildId: string) => .select((eb) => eb.fn.count("id").as("count")) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .where("reason", "=", ReportReasons.anonReport), kysely .selectFrom("reported_messages") @@ -269,7 +261,6 @@ export const getUserReportSummary = (userId: string, guildId: string) => ]) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .groupBy(({ fn }) => fn("date", ["created_at"])) .orderBy("count", "desc") .limit(1), @@ -278,7 +269,6 @@ export const getUserReportSummary = (userId: string, guildId: string) => .select(({ fn }) => fn.count("staff_id").distinct().as("count")) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .where("staff_id", "is not", null), ]); @@ -324,7 +314,6 @@ export const getMonthlyReportCounts = ( ]) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .where("created_at", ">=", cutoff) .groupBy(({ fn, val, ref }) => fn("strftime", [val("%Y-%m"), ref("created_at")]), @@ -355,14 +344,12 @@ export const getRecentReportCount = ( .selectFrom("reported_messages") .select((eb) => eb.fn.count("id").as("count")) .where("reported_user_id", "=", userId) - .where("guild_id", "=", guildId) - .where("deleted_at", "is", null), + .where("guild_id", "=", guildId), kysely .selectFrom("reported_messages") .select((eb) => eb.fn.count("id").as("count")) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .where("created_at", ">=", cutoff), ]); @@ -394,7 +381,6 @@ export const getChannelBreakdown = ( .select((eb) => eb.fn.count("id").as("count")) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .groupBy("reported_channel_id") .orderBy("count", "desc") .limit(limit); @@ -417,7 +403,6 @@ export const getStaffBreakdown = (userId: string, guildId: string, limit = 5) => .select((eb) => eb.fn.count("id").as("count")) .where("reported_user_id", "=", userId) .where("guild_id", "=", guildId) - .where("deleted_at", "is", null) .where("staff_id", "is not", null) .groupBy(["staff_id", "staff_username"]) .orderBy("count", "desc")