Files
GMW/services/discord-gateway/src/modules/ai-moderation/moderationBuilders.ts
T
asepharyana 1f91f99de3 feat(automod): render sticker, role & user names in moderation views
QoL lanjutan dari fix60084b3: content pesan mentah masih nampilin
snowflake (<@&roleid>, <@userid>, <:emoji:id>) di log moderasi dan
prompt LLM. Sekarang dirender ke nama yang bisa dibaca:

- Gateway capture: metadata menyimpan mentionedRoles + mentionedUsers
  (id+name) dari message.mentions, disimpan ke metadata JSON
- renderDiscordMentions(): <@&id> -> @RoleName, <@id> -> @Username,
  <:name:id> -> :name:, fallback @role/@user — dipakai di
  conversationContext (konteks LLM) dan moderationBuilders
  (getAnalysisContent) sehingga LLM lihat nama role/user beneran,
  bukan placeholder generik
- Frontend renderMessageContent() (mirror gateway) dipasang di semua
  tempat nampilin content: message-card, message-detail(-view),
  search-overlay, search-panel, users/channels section, live-stream,
  mod-queue, review list; sticker-only message tetap [Sticker: name],
  pesan teks+sticker kini ikut nampilin nama sticker
- tsc --noEmit PASS di gateway & frontend; renderDiscordMentions
  diverifikasi manual (6 kasus: role/user/emoji/unknown/plain)
2026-08-01 08:56:10 +07:00

91 lines
3.1 KiB
TypeScript

/**
* moderationBuilders.ts
*
* Shared builder utilities extracted from llmModerationClient.ts.
* Used by both mediaAnalysisClient.ts and moderationOrchestrator.ts.
*/
import { messageStore } from "../message-capture/messageStore.js";
import type { MessageRecord } from "../message-capture/types.js";
import { renderDiscordMentions } from "../message-capture/messageMetadata.js";
import { sanitizeDiscordTokens } from "./discordTokens.js";
/** Simple XML-escaping for content text. */
export function escapeXml(s: string): string {
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
/**
* Returns the real text content for AI analysis, stripping fallback text
* that getDisplayContent() synthesized ("[Attachment: ...]", "[Sticker: ...]",
* "[Embed]"). These filenames alone are meaningless to the LLM and can
* falsely inflate a "clean" verdict when the actual image failed to download.
*/
export function getAnalysisContent(message: MessageRecord): string {
const raw = message.edited_content ?? message.content;
const stripped = raw.replace(
/\[(?:Attachment|Sticker):[^\]]*\]|\[Embed\]/g,
"",
);
return sanitizeDiscordTokens(
renderDiscordMentions(stripped, message.metadata),
).trim();
}
/**
* Builds a <reference> XML element for reply/forward/crosspost context.
*/
export async function buildReferenceXml(msg: MessageRecord): Promise<string> {
const parts: string[] = [];
if (msg.is_reply && msg.reference_message_id) {
parts.push(`type="reply"`);
} else if (msg.is_forward && msg.reference_message_id) {
parts.push(`type="forward"`);
}
if (msg.is_crosspost) {
parts.push(`type="crosspost"`);
}
if (!msg.reference_message_id) return "";
let parentContent = "";
if (msg.reference_message_id) {
// 1. Try DB first — works for messages captured in the same server
try {
const parent = await messageStore.getMessageById(
msg.reference_message_id,
);
if (parent) {
const parentText = parent.edited_content ?? parent.content;
parentContent = parentText.slice(0, 500);
}
} catch {
// Parent fetch failed — fall through to metadata
}
// 2. Fall back to metadata — works for forwards from other servers/channels
// where the original message was never captured in this DB.
// The capture phase stores the snapshot content in metadata.reference.content.
if (!parentContent && msg.metadata) {
try {
const meta = JSON.parse(msg.metadata);
const refContent = meta?.reference?.content;
if (refContent && typeof refContent === "string") {
parentContent = refContent.slice(0, 500);
}
} catch {
// Metadata parse failed — no fallback available
}
}
}
const attr = parts.join(" ");
const parentXml = parentContent
? `<parent_content>${escapeXml(parentContent)}</parent_content>`
: "";
return `<reference ${attr} message_id="${msg.reference_message_id}" channel_id="${msg.reference_channel_id ?? ""}" guild_id="${msg.reference_guild_id ?? ""}">${parentXml}</reference>`;
}