fix: capture server nickname (member displayName) per action
Rename server_name (guild name) to server_nick and populate it from the member's server-specific display name (metadata.member.displayName) at write time. This is what the moderation dashboard should show as TARGET — e.g. server nick 'Bandar Togel「✔ ᵛᵉʳᶦᶠᶦᵉᵈ 」' for global username '.nichiyobi'. Backfilled 210 existing actions from messages metadata (reset_nickname rows now show 'Sarjana .jav', 'Penindas Minoritas', etc). Frontend TARGET shows server nick with global username as secondary context.
This commit is contained in:
@@ -125,7 +125,7 @@ export class ModerationRepository {
|
|||||||
a.evidence,
|
a.evidence,
|
||||||
a.policy_version,
|
a.policy_version,
|
||||||
a.username,
|
a.username,
|
||||||
a.server_name,
|
a.server_nick,
|
||||||
LEFT(m.content, 300) AS content
|
LEFT(m.content, 300) AS content
|
||||||
FROM moderation_actions a
|
FROM moderation_actions a
|
||||||
LEFT JOIN messages m ON m.id = a.message_id
|
LEFT JOIN messages m ON m.id = a.message_id
|
||||||
@@ -156,7 +156,7 @@ export class ModerationRepository {
|
|||||||
evidence: parseJsonArray(r.evidence),
|
evidence: parseJsonArray(r.evidence),
|
||||||
policy_version: r.policy_version ? String(r.policy_version) : null,
|
policy_version: r.policy_version ? String(r.policy_version) : null,
|
||||||
username: r.username ? String(r.username) : 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,
|
content: r.content ? String(r.content) : null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -340,7 +340,7 @@ export class ModerationRepository {
|
|||||||
confidence: r.confidence != null ? Number(r.confidence) : null,
|
confidence: r.confidence != null ? Number(r.confidence) : null,
|
||||||
score: r.score != null ? Number(r.score) : null,
|
score: r.score != null ? Number(r.score) : null,
|
||||||
username: r.username ? String(r.username) : 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,
|
content: r.content ? String(r.content) : null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -134,6 +134,13 @@
|
|||||||
"when": 1787820000000,
|
"when": 1787820000000,
|
||||||
"tag": "0018_add_server_name_to_moderation_actions",
|
"tag": "0018_add_server_name_to_moderation_actions",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 19,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1787830000000,
|
||||||
|
"tag": "0019_rename_server_name_to_server_nick",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import type { Client, PermissionString } from "discord.js-selfbot-v13";
|
|||||||
import { LRUCache } from "lru-cache";
|
import { LRUCache } from "lru-cache";
|
||||||
import { createChildLogger } from "@/shared/logger/index";
|
import { createChildLogger } from "@/shared/logger/index";
|
||||||
import { config } from "../../shared/config/config.js";
|
import { config } from "../../shared/config/config.js";
|
||||||
|
import { parseRichMessageMetadata } from "../message-capture/messageMetadata.js";
|
||||||
import { messageStore } from "../message-capture/messageStore.js";
|
import { messageStore } from "../message-capture/messageStore.js";
|
||||||
import type { MessageRecord } from "../message-capture/types.js";
|
import type { MessageRecord } from "../message-capture/types.js";
|
||||||
import {
|
import {
|
||||||
@@ -269,10 +270,24 @@ function hasPermissionApi(channel: unknown): channel is {
|
|||||||
|
|
||||||
// ─── Database Action Log ─────────────────────────────────────────────
|
// ─── 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(
|
async function logAutoDeleteAttempt(
|
||||||
message: MessageRecord,
|
message: MessageRecord,
|
||||||
result: AutoDeleteResult,
|
result: AutoDeleteResult,
|
||||||
serverName?: string | null,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await messageStore.createModerationAction({
|
await messageStore.createModerationAction({
|
||||||
@@ -283,7 +298,7 @@ async function logAutoDeleteAttempt(
|
|||||||
reason: result.reason,
|
reason: result.reason,
|
||||||
...verdictToActionFields(message),
|
...verdictToActionFields(message),
|
||||||
username: message.username,
|
username: message.username,
|
||||||
server_name: serverName ?? null,
|
server_nick: resolveServerNick(message),
|
||||||
executed_by: "auto-delete-manager",
|
executed_by: "auto-delete-manager",
|
||||||
status: result.deleted
|
status: result.deleted
|
||||||
? "executed"
|
? "executed"
|
||||||
@@ -350,7 +365,7 @@ export async function attemptAutoDeleteFlaggedMessage(
|
|||||||
"nickname melanggar aturan server (offensive_username); pesan dibiarkan",
|
"nickname melanggar aturan server (offensive_username); pesan dibiarkan",
|
||||||
...verdictToActionFields(message),
|
...verdictToActionFields(message),
|
||||||
username: message.username,
|
username: message.username,
|
||||||
server_name: null,
|
server_nick: resolveServerNick(message),
|
||||||
executed_by: "auto-delete-manager",
|
executed_by: "auto-delete-manager",
|
||||||
status: resetOk ? "executed" : "failed",
|
status: resetOk ? "executed" : "failed",
|
||||||
error: resetOk ? null : "nickname_reset_failed",
|
error: resetOk ? null : "nickname_reset_failed",
|
||||||
@@ -471,7 +486,7 @@ export async function attemptAutoDeleteFlaggedMessage(
|
|||||||
skipped: true,
|
skipped: true,
|
||||||
reason: "dry_run",
|
reason: "dry_run",
|
||||||
};
|
};
|
||||||
await logAutoDeleteAttempt(message, result, guild.name);
|
await logAutoDeleteAttempt(message, result);
|
||||||
logger.info(
|
logger.info(
|
||||||
{ messageId: message.id, channelId },
|
{ messageId: message.id, channelId },
|
||||||
"Auto-delete dry-run: would delete flagged message",
|
"Auto-delete dry-run: would delete flagged message",
|
||||||
@@ -503,7 +518,7 @@ export async function attemptAutoDeleteFlaggedMessage(
|
|||||||
skipped: false,
|
skipped: false,
|
||||||
reason: "deleted",
|
reason: "deleted",
|
||||||
};
|
};
|
||||||
await logAutoDeleteAttempt(message, result, guild.name);
|
await logAutoDeleteAttempt(message, result);
|
||||||
logger.info(
|
logger.info(
|
||||||
{ messageId: message.id, channelId },
|
{ messageId: message.id, channelId },
|
||||||
"Auto-deleted AI-flagged message",
|
"Auto-deleted AI-flagged message",
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export class ModerationHandler {
|
|||||||
| "ban_user",
|
| "ban_user",
|
||||||
reason: payload.reason ?? null,
|
reason: payload.reason ?? null,
|
||||||
username: null,
|
username: null,
|
||||||
server_name: null,
|
server_nick: null,
|
||||||
executed_by: payload.executed_by ?? "command-handler",
|
executed_by: payload.executed_by ?? "command-handler",
|
||||||
status: "executed",
|
status: "executed",
|
||||||
error: null,
|
error: null,
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ export const pgModerationActionsTable = pgTable(
|
|||||||
evidence: pgText("evidence"), // JSON array of short quoted snippets
|
evidence: pgText("evidence"), // JSON array of short quoted snippets
|
||||||
policy_version: pgText("policy_version"), // rules.ts policy version string
|
policy_version: pgText("policy_version"), // rules.ts policy version string
|
||||||
username: pgText("username"), // denormalized from messages table for retention safety
|
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) => ({
|
(table) => ({
|
||||||
messageIdIdx: pgIndex("idx_moderation_actions_message_id").on(
|
messageIdIdx: pgIndex("idx_moderation_actions_message_id").on(
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const pgModerationActionsTable = pgTable(
|
|||||||
evidence: pgText("evidence"), // JSON array of short quoted snippets
|
evidence: pgText("evidence"), // JSON array of short quoted snippets
|
||||||
policy_version: pgText("policy_version"), // rules.ts policy version string
|
policy_version: pgText("policy_version"), // rules.ts policy version string
|
||||||
username: pgText("username"), // denormalized from messages table for retention safety
|
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) => ({
|
(table) => ({
|
||||||
messageIdIdx: pgIndex("idx_moderation_actions_message_id").on(
|
messageIdIdx: pgIndex("idx_moderation_actions_message_id").on(
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export interface ModerationAction {
|
|||||||
action_type: ModerationActionType;
|
action_type: ModerationActionType;
|
||||||
reason: string | null;
|
reason: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
server_name: string | null;
|
server_nick: string | null;
|
||||||
executed_by: string | null;
|
executed_by: string | null;
|
||||||
status: "pending" | "executed" | "failed";
|
status: "pending" | "executed" | "failed";
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
|||||||
@@ -149,14 +149,15 @@ export function LiveModerationFeed({
|
|||||||
<div className="mt-1.5 flex items-center gap-2 font-mono text-[10px] text-ink-faint">
|
<div className="mt-1.5 flex items-center gap-2 font-mono text-[10px] text-ink-faint">
|
||||||
<span>TARGET:</span>
|
<span>TARGET:</span>
|
||||||
<span className="text-ink-soft">
|
<span className="text-ink-soft">
|
||||||
{a.username ?? a.user_id ?? "UNKNOWN_SUBJECT"}
|
{a.server_nick ??
|
||||||
|
a.username ??
|
||||||
|
a.user_id ??
|
||||||
|
"UNKNOWN_SUBJECT"}
|
||||||
</span>
|
</span>
|
||||||
{a.server_name && (
|
{a.username && a.username !== a.server_nick && (
|
||||||
<>
|
<>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span className="text-ink-soft">
|
<span className="text-ink-soft">@{a.username}</span>
|
||||||
{a.server_name}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface ModerationAction {
|
|||||||
created_at: number | null;
|
created_at: number | null;
|
||||||
executed_at: number | null;
|
executed_at: number | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
server_name: string | null;
|
server_nick: string | null;
|
||||||
content: string | null;
|
content: string | null;
|
||||||
// ── Explainability (structured verdict, surfaced read-only to public web) ──
|
// ── Explainability (structured verdict, surfaced read-only to public web) ──
|
||||||
flags: string[] | null;
|
flags: string[] | null;
|
||||||
@@ -81,7 +81,7 @@ export interface CategoryAction {
|
|||||||
confidence: number | null;
|
confidence: number | null;
|
||||||
score: number | null;
|
score: number | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
server_name: string | null;
|
server_nick: string | null;
|
||||||
content: string | null;
|
content: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user