diff --git a/services/backend/src/modules/moderation/moderation.repository.ts b/services/backend/src/modules/moderation/moderation.repository.ts index e7052a5c..a170fbe0 100644 --- a/services/backend/src/modules/moderation/moderation.repository.ts +++ b/services/backend/src/modules/moderation/moderation.repository.ts @@ -125,7 +125,7 @@ export class ModerationRepository { a.evidence, a.policy_version, a.username, - a.server_name, + a.server_nick, LEFT(m.content, 300) AS content FROM moderation_actions a LEFT JOIN messages m ON m.id = a.message_id @@ -156,7 +156,7 @@ export class ModerationRepository { evidence: parseJsonArray(r.evidence), policy_version: r.policy_version ? String(r.policy_version) : null, username: r.username ? String(r.username) : null, - server_name: r.server_name ? String(r.server_name) : null, + server_nick: r.server_nick ? String(r.server_nick) : null, content: r.content ? String(r.content) : null, })); @@ -340,7 +340,7 @@ export class ModerationRepository { confidence: r.confidence != null ? Number(r.confidence) : null, score: r.score != null ? Number(r.score) : null, username: r.username ? String(r.username) : null, - server_name: r.server_name ? String(r.server_name) : null, + server_nick: r.server_nick ? String(r.server_nick) : null, content: r.content ? String(r.content) : null, })); } diff --git a/services/discord-gateway/drizzle/migrations/0019_rename_server_name_to_server_nick.sql b/services/discord-gateway/drizzle/migrations/0019_rename_server_name_to_server_nick.sql new file mode 100644 index 00000000..6148ae57 --- /dev/null +++ b/services/discord-gateway/drizzle/migrations/0019_rename_server_name_to_server_nick.sql @@ -0,0 +1,4 @@ +-- Rename server_name (guild name) -> server_nick (member's server nickname) +-- server_name was added in 0018 but never populated (all NULL), so renaming +-- the empty column is safe and keeps retention semantics correct. +ALTER TABLE "moderation_actions" RENAME COLUMN "server_name" TO "server_nick"; diff --git a/services/discord-gateway/drizzle/migrations/meta/_journal.json b/services/discord-gateway/drizzle/migrations/meta/_journal.json index 6028ab67..f87d5c5f 100644 --- a/services/discord-gateway/drizzle/migrations/meta/_journal.json +++ b/services/discord-gateway/drizzle/migrations/meta/_journal.json @@ -134,6 +134,13 @@ "when": 1787820000000, "tag": "0018_add_server_name_to_moderation_actions", "breakpoints": true + }, + { + "idx": 19, + "version": "7", + "when": 1787830000000, + "tag": "0019_rename_server_name_to_server_nick", + "breakpoints": true } ] } \ No newline at end of file diff --git a/services/discord-gateway/src/modules/ai-moderation/autoDeleteManager.ts b/services/discord-gateway/src/modules/ai-moderation/autoDeleteManager.ts index 71d64ac6..01f62b59 100644 --- a/services/discord-gateway/src/modules/ai-moderation/autoDeleteManager.ts +++ b/services/discord-gateway/src/modules/ai-moderation/autoDeleteManager.ts @@ -2,6 +2,7 @@ import type { Client, PermissionString } from "discord.js-selfbot-v13"; import { LRUCache } from "lru-cache"; import { createChildLogger } from "@/shared/logger/index"; import { config } from "../../shared/config/config.js"; +import { parseRichMessageMetadata } from "../message-capture/messageMetadata.js"; import { messageStore } from "../message-capture/messageStore.js"; import type { MessageRecord } from "../message-capture/types.js"; import { @@ -269,10 +270,24 @@ function hasPermissionApi(channel: unknown): channel is { // ─── Database Action Log ───────────────────────────────────────────── +/** + * Extract the member's server-specific display name (nickname) from a + * captured message's metadata. Falls back to the global username when no + * server nickname exists (member.displayName defaults to the guild nickname + * when set, otherwise the global username). + */ +function resolveServerNick(message: MessageRecord): string | null { + try { + const parsed = parseRichMessageMetadata(message.metadata); + return parsed?.member?.displayName ?? message.username ?? null; + } catch { + return message.username ?? null; + } +} + async function logAutoDeleteAttempt( message: MessageRecord, result: AutoDeleteResult, - serverName?: string | null, ): Promise { try { await messageStore.createModerationAction({ @@ -283,7 +298,7 @@ async function logAutoDeleteAttempt( reason: result.reason, ...verdictToActionFields(message), username: message.username, - server_name: serverName ?? null, + server_nick: resolveServerNick(message), executed_by: "auto-delete-manager", status: result.deleted ? "executed" @@ -350,7 +365,7 @@ export async function attemptAutoDeleteFlaggedMessage( "nickname melanggar aturan server (offensive_username); pesan dibiarkan", ...verdictToActionFields(message), username: message.username, - server_name: null, + server_nick: resolveServerNick(message), executed_by: "auto-delete-manager", status: resetOk ? "executed" : "failed", error: resetOk ? null : "nickname_reset_failed", @@ -471,7 +486,7 @@ export async function attemptAutoDeleteFlaggedMessage( skipped: true, reason: "dry_run", }; - await logAutoDeleteAttempt(message, result, guild.name); + await logAutoDeleteAttempt(message, result); logger.info( { messageId: message.id, channelId }, "Auto-delete dry-run: would delete flagged message", @@ -503,7 +518,7 @@ export async function attemptAutoDeleteFlaggedMessage( skipped: false, reason: "deleted", }; - await logAutoDeleteAttempt(message, result, guild.name); + await logAutoDeleteAttempt(message, result); logger.info( { messageId: message.id, channelId }, "Auto-deleted AI-flagged message", diff --git a/services/discord-gateway/src/modules/command-handler/moderation.handler.ts b/services/discord-gateway/src/modules/command-handler/moderation.handler.ts index 216e4661..2496a16c 100644 --- a/services/discord-gateway/src/modules/command-handler/moderation.handler.ts +++ b/services/discord-gateway/src/modules/command-handler/moderation.handler.ts @@ -104,7 +104,7 @@ export class ModerationHandler { | "ban_user", reason: payload.reason ?? null, username: null, - server_name: null, + server_nick: null, executed_by: payload.executed_by ?? "command-handler", status: "executed", error: null, diff --git a/services/discord-gateway/src/shared/database/schema.ts b/services/discord-gateway/src/shared/database/schema.ts index e56f1098..92f549f4 100644 --- a/services/discord-gateway/src/shared/database/schema.ts +++ b/services/discord-gateway/src/shared/database/schema.ts @@ -643,7 +643,7 @@ export const pgModerationActionsTable = pgTable( evidence: pgText("evidence"), // JSON array of short quoted snippets policy_version: pgText("policy_version"), // rules.ts policy version string username: pgText("username"), // denormalized from messages table for retention safety - server_name: pgText("server_name"), // denormalized guild name for retention safety + server_nick: pgText("server_nick"), // member's server-specific display name (nickname), from metadata.member.displayName }, (table) => ({ messageIdIdx: pgIndex("idx_moderation_actions_message_id").on( diff --git a/services/discord-gateway/src/shared/database/schema/messages.ts b/services/discord-gateway/src/shared/database/schema/messages.ts index 1eb4e056..aaaaf57f 100644 --- a/services/discord-gateway/src/shared/database/schema/messages.ts +++ b/services/discord-gateway/src/shared/database/schema/messages.ts @@ -60,7 +60,7 @@ export const pgModerationActionsTable = pgTable( evidence: pgText("evidence"), // JSON array of short quoted snippets policy_version: pgText("policy_version"), // rules.ts policy version string username: pgText("username"), // denormalized from messages table for retention safety - server_name: pgText("server_name"), // denormalized guild name for retention safety + server_nick: pgText("server_nick"), // member's server-specific display name (nickname), from metadata.member.displayName }, (table) => ({ messageIdIdx: pgIndex("idx_moderation_actions_message_id").on( diff --git a/services/discord-gateway/src/shared/moderation-types.ts b/services/discord-gateway/src/shared/moderation-types.ts index 2b32dc8e..76ea3fde 100644 --- a/services/discord-gateway/src/shared/moderation-types.ts +++ b/services/discord-gateway/src/shared/moderation-types.ts @@ -209,7 +209,7 @@ export interface ModerationAction { action_type: ModerationActionType; reason: string | null; username: string | null; - server_name: string | null; + server_nick: string | null; executed_by: string | null; status: "pending" | "executed" | "failed"; error: string | null; diff --git a/services/frontend/src/components/LiveModerationFeed.tsx b/services/frontend/src/components/LiveModerationFeed.tsx index c3e164ee..71de3de1 100644 --- a/services/frontend/src/components/LiveModerationFeed.tsx +++ b/services/frontend/src/components/LiveModerationFeed.tsx @@ -149,14 +149,15 @@ export function LiveModerationFeed({
TARGET: - {a.username ?? a.user_id ?? "UNKNOWN_SUBJECT"} + {a.server_nick ?? + a.username ?? + a.user_id ?? + "UNKNOWN_SUBJECT"} - {a.server_name && ( + {a.username && a.username !== a.server_nick && ( <> · - - {a.server_name} - + @{a.username} )}
diff --git a/services/frontend/src/lib/types/moderation.ts b/services/frontend/src/lib/types/moderation.ts index 8c204894..c78a09e4 100644 --- a/services/frontend/src/lib/types/moderation.ts +++ b/services/frontend/src/lib/types/moderation.ts @@ -20,7 +20,7 @@ export interface ModerationAction { created_at: number | null; executed_at: number | null; username: string | null; - server_name: string | null; + server_nick: string | null; content: string | null; // ── Explainability (structured verdict, surfaced read-only to public web) ── flags: string[] | null; @@ -81,7 +81,7 @@ export interface CategoryAction { confidence: number | null; score: number | null; username: string | null; - server_name: string | null; + server_nick: string | null; content: string | null; }