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)
This commit is contained in:
asepharyana
2026-08-01 08:56:10 +07:00
parent 6df4f306dd
commit 1f91f99de3
16 changed files with 182 additions and 18 deletions
+64
View File
@@ -1,3 +1,5 @@
import type { MessageMetadata } from "@/lib/types/message";
/**
* Format a number with locale separators.
*/
@@ -72,3 +74,65 @@ export function getMessageChannelLabel(msg: {
if (channelName) return channelName;
return msg.channel_id?.slice(0, 8) ?? "";
}
/**
* Render Discord mention/emoji/sticker tokens in message content to readable
* names using the captured metadata (mentionedRoles / mentionedUsers /
* customEmojis / stickers). Mirror of the gateway's renderDiscordMentions.
*
* - `<@&id>` → `@RoleName` (falls back to `@role`)
* - `<@id>` / `<@!id>` → `@Username` (falls back to `@user`)
* - `<:name:id>` → `:name:` (falls back to the literal name)
* - sticker-only content already stored as `[Sticker: name]`; when a message
* has both text and stickers, append `[Sticker: name]` so stickers always
* surface in the feed.
*/
export function renderMessageContent(
content: string,
metadata?: string | null,
): string {
if (!content) return content;
let m: MessageMetadata | null = null;
try {
m = JSON.parse(metadata ?? "") as MessageMetadata;
} catch {
// metadata malformed — render tokens from their literal names only
}
let rendered = content;
if (rendered.includes("<")) {
const roles = new Map(
(m?.mentionedRoles ?? []).map((r) => [r.id, r.name] as const),
);
const users = new Map(
(m?.mentionedUsers ?? []).map((u) => [u.id, u.username] as const),
);
const emojis = new Map(
(m?.customEmojis ?? []).map((e) => [e.id, e.name] as const),
);
rendered = rendered.replace(
/<(?:a)?:([a-zA-Z0-9_]+):(\d{17,20})>|<@!?(\d{17,20})>|<@&(\d{17,20})>/g,
(_full, emojiTokenName, emojiId, userId, roleId) => {
if (emojiId !== undefined) {
return `:${emojis.get(emojiId) ?? emojiTokenName}:`;
}
if (roleId !== undefined) return `@${roles.get(roleId) ?? "role"}`;
if (userId !== undefined) return `@${users.get(userId) ?? "user"}`;
return _full;
},
);
}
const stickers = m?.stickers ?? [];
if (
stickers.length > 0 &&
!rendered.includes("[Sticker:") &&
!rendered.includes("[Attachment:")
) {
rendered += ` ${stickers
.map((s) => `[Sticker: ${s.name ?? "unknown"}]`)
.join(" ")}`;
}
return rendered;
}
@@ -43,6 +43,23 @@ export interface StickerInfo {
url?: string | null;
}
export interface CustomEmojiInfo {
id: string;
name: string;
animated?: boolean;
url?: string | null;
}
export interface MentionedRoleInfo {
id: string;
name: string;
}
export interface MentionedUserInfo {
id: string;
username: string;
}
export interface AttachmentRef {
name: string;
url: string;
@@ -70,6 +87,9 @@ export interface MessageMetadata {
stickers?: StickerInfo[] | null;
attachments?: AttachmentRef[] | null;
embeds?: EmbedInfo[] | null;
customEmojis?: CustomEmojiInfo[] | null;
mentionedRoles?: MentionedRoleInfo[] | null;
mentionedUsers?: MentionedUserInfo[] | null;
channel?: ChannelRef | null;
reference?: ReferenceInfo | null;
}