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
@@ -1,6 +1,9 @@
import { createChildLogger } from "@/shared/logger/index"; import { createChildLogger } from "@/shared/logger/index";
import { encoding_for_model as encodingForModel } from "tiktoken"; import { encoding_for_model as encodingForModel } from "tiktoken";
import { formatMediaEvidenceForPrompt } from "../message-capture/messageMetadata.js"; import {
formatMediaEvidenceForPrompt,
renderDiscordMentions,
} from "../message-capture/messageMetadata.js";
import type { MessageRecord } from "../message-capture/types.js"; import type { MessageRecord } from "../message-capture/types.js";
import { sanitizeDiscordTokens } from "./discordTokens.js"; import { sanitizeDiscordTokens } from "./discordTokens.js";
@@ -101,7 +104,7 @@ export function formatMessageForPrompt(
label: "context" | "target", label: "context" | "target",
): string { ): string {
const content = sanitizeDiscordTokens( const content = sanitizeDiscordTokens(
msg.edited_content ?? msg.content, renderDiscordMentions(msg.edited_content ?? msg.content, msg.metadata),
); );
const timestamp = formatTimestamp(msg.created_at); const timestamp = formatTimestamp(msg.created_at);
const mediaEvidence = formatMediaEvidenceForPrompt(msg.metadata); const mediaEvidence = formatMediaEvidenceForPrompt(msg.metadata);
@@ -7,6 +7,7 @@
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 { renderDiscordMentions } from "../message-capture/messageMetadata.js";
import { sanitizeDiscordTokens } from "./discordTokens.js"; import { sanitizeDiscordTokens } from "./discordTokens.js";
/** Simple XML-escaping for content text. */ /** Simple XML-escaping for content text. */
@@ -30,7 +31,9 @@ export function getAnalysisContent(message: MessageRecord): string {
/\[(?:Attachment|Sticker):[^\]]*\]|\[Embed\]/g, /\[(?:Attachment|Sticker):[^\]]*\]|\[Embed\]/g,
"", "",
); );
return sanitizeDiscordTokens(stripped).trim(); return sanitizeDiscordTokens(
renderDiscordMentions(stripped, message.metadata),
).trim();
} }
/** /**
@@ -28,6 +28,16 @@ export interface CustomEmojiEvidence {
url: string; url: string;
} }
export interface MentionedRoleEvidence {
id: string;
name: string;
}
export interface MentionedUserEvidence {
id: string;
username: string;
}
export interface EmbedEvidence { export interface EmbedEvidence {
title: string | null; title: string | null;
description: string | null; description: string | null;
@@ -64,6 +74,8 @@ export interface RichMessageMetadata {
embeds: Array<EmbedEvidence>; embeds: Array<EmbedEvidence>;
attachments: Array<AttachmentEvidence>; attachments: Array<AttachmentEvidence>;
customEmojis: Array<CustomEmojiEvidence>; customEmojis: Array<CustomEmojiEvidence>;
mentionedRoles: Array<MentionedRoleEvidence>;
mentionedUsers: Array<MentionedUserEvidence>;
author: { author: {
id: string; id: string;
username: string; username: string;
@@ -268,6 +280,12 @@ export function getMessageMetadata(message: Message): RichMessageMetadata {
embeds: getEmbedMetadata(message), embeds: getEmbedMetadata(message),
attachments: getAttachmentMetadata(message), attachments: getAttachmentMetadata(message),
customEmojis: getCustomEmojiMetadata(message), customEmojis: getCustomEmojiMetadata(message),
mentionedRoles: Array.from(message.mentions?.roles?.values() ?? []).map(
(role) => ({ id: role.id, name: role.name }),
),
mentionedUsers: Array.from(message.mentions?.users?.values() ?? []).map(
(user) => ({ id: user.id, username: user.username }),
),
author: { author: {
id: message.author.id, id: message.author.id,
username: message.author.username, username: message.author.username,
@@ -315,6 +333,12 @@ export function parseRichMessageMetadata(
customEmojis: Array.isArray(parsed.customEmojis) customEmojis: Array.isArray(parsed.customEmojis)
? parsed.customEmojis ? parsed.customEmojis
: [], : [],
mentionedRoles: Array.isArray(parsed.mentionedRoles)
? parsed.mentionedRoles
: [],
mentionedUsers: Array.isArray(parsed.mentionedUsers)
? parsed.mentionedUsers
: [],
author: parsed.author as RichMessageMetadata["author"], author: parsed.author as RichMessageMetadata["author"],
member: (parsed.member ?? null) as RichMessageMetadata["member"], member: (parsed.member ?? null) as RichMessageMetadata["member"],
channel: parsed.channel as RichMessageMetadata["channel"], channel: parsed.channel as RichMessageMetadata["channel"],
@@ -463,3 +487,44 @@ export function getDisplayContent(message: Message): string {
return ""; return "";
} }
/**
* Renders Discord mention/emoji tokens in message content to readable names
* using the captured metadata (mentionedRoles / mentionedUsers / customEmojis).
*
* - `<@&id>` → `@RoleName` (falls back to `@role`)
* - `<@id>` / `<@!id>` → `@Username` (falls back to `@user`)
* - `<:name:id>` → `:name:` (falls back to the literal name)
*
* Unresolvable tokens keep Discord's own name from the token, so no numeric
* snowflake ever reaches the reader. Content without "<" is returned untouched.
* Used by both the LLM prompt pipeline (conversationContext / moderationBuilders)
* and mirrored in the frontend (lib/format.ts renderMessageContent).
*/
export function renderDiscordMentions(
content: string,
metadata: string | null | undefined,
): string {
if (!content || !content.includes("<")) return content;
const parsed = parseRichMessageMetadata(metadata);
const roleName = new Map(
(parsed?.mentionedRoles ?? []).map((r) => [r.id, r.name] as const),
);
const userName = new Map(
(parsed?.mentionedUsers ?? []).map((u) => [u.id, u.username] as const),
);
const emojiName = new Map(
(parsed?.customEmojis ?? []).map((e) => [e.id, e.name] as const),
);
return content.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 `:${emojiName.get(emojiId) ?? emojiTokenName}:`;
}
if (roleId !== undefined) return `@${roleName.get(roleId) ?? "role"}`;
if (userId !== undefined) return `@${userName.get(userId) ?? "user"}`;
return _full;
},
);
}
@@ -31,6 +31,7 @@ export default function DashboardPage() {
id: msg.id, id: msg.id,
content: msg.content || msg.id, content: msg.content || msg.id,
username: msg.username, username: msg.username,
metadata: msg.metadata ?? null,
severity: severity:
msg.ai_severity && msg.ai_severity !== "none" msg.ai_severity && msg.ai_severity !== "none"
? (msg.ai_severity as ModQueueItem["severity"]) ? (msg.ai_severity as ModQueueItem["severity"])
@@ -7,6 +7,7 @@ import { MessageList } from "@/components/messages/message-list";
import { MessageDetailView } from "@/components/messages/message-detail-view"; import { MessageDetailView } from "@/components/messages/message-detail-view";
import { SearchOverlay } from "@/components/messages/search-overlay"; import { SearchOverlay } from "@/components/messages/search-overlay";
import { extractFirstImage } from "@/components/messages/message-card"; import { extractFirstImage } from "@/components/messages/message-card";
import { renderMessageContent } from "@/lib/format";
import { SubNav } from "@/components/layout/sub-nav"; import { SubNav } from "@/components/layout/sub-nav";
import { ErrorState, LoadingSkeleton } from "@/components/shared"; import { ErrorState, LoadingSkeleton } from "@/components/shared";
import { GlassCard } from "@/components/glass/card"; import { GlassCard } from "@/components/glass/card";
@@ -317,7 +318,7 @@ function ReviewList({
<Flag className="mt-0.5 size-3.5 shrink-0 text-accent-purple" /> <Flag className="mt-0.5 size-3.5 shrink-0 text-accent-purple" />
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="line-clamp-2 text-xs text-text-secondary"> <p className="line-clamp-2 text-xs text-text-secondary">
{item.content || item.id} {renderMessageContent(item.content, item.metadata) || item.id}
</p> </p>
</div> </div>
</div> </div>
@@ -13,7 +13,7 @@ import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { useReanalyze } from "@/hooks"; import { useReanalyze } from "@/hooks";
import { messagesApi } from "@/lib/api"; import { messagesApi } from "@/lib/api";
import { safeParseJsonArray } from "@/lib/format"; import { renderMessageContent, safeParseJsonArray } from "@/lib/format";
import type { MessageRecord } from "@/lib/types"; import type { MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -100,7 +100,7 @@ export function SearchPanel() {
</Badge> </Badge>
)} )}
</div> </div>
<p className="text-sm leading-relaxed">{msg.content}</p> <p className="text-sm leading-relaxed">{renderMessageContent(msg.content, msg.metadata)}</p>
{msg.ai_moderation_flags && {msg.ai_moderation_flags &&
msg.ai_moderation_flags !== "[]" && ( msg.ai_moderation_flags !== "[]" && (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useChannelDetail, useChannels } from "@/hooks"; import { useChannelDetail, useChannels } from "@/hooks";
import { renderMessageContent } from "@/lib/format";
import type { DashboardChannel } from "@/lib/types"; import type { DashboardChannel } from "@/lib/types";
export function ChannelsSection({ guildId }: { guildId?: string }) { export function ChannelsSection({ guildId }: { guildId?: string }) {
@@ -123,7 +124,7 @@ export function ChannelsSection({ guildId }: { guildId?: string }) {
className="rounded-lg border border-border/40 bg-card/40 px-3 py-2" className="rounded-lg border border-border/40 bg-card/40 px-3 py-2"
> >
<p className="text-xs leading-relaxed text-text-secondary line-clamp-2"> <p className="text-xs leading-relaxed text-text-secondary line-clamp-2">
{msg.username}: {msg.content || "(no text content)"} {msg.username}: {renderMessageContent(msg.content, msg.metadata) || "(no text content)"}
</p> </p>
<p className="mt-1 text-[10px] font-mono text-text-secondary/40"> <p className="mt-1 text-[10px] font-mono text-text-secondary/40">
{new Date(msg.created_at).toLocaleString()} {new Date(msg.created_at).toLocaleString()}
@@ -2,6 +2,7 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { GlassCard } from "@/components/glass/card"; import { GlassCard } from "@/components/glass/card";
import { renderMessageContent } from "@/lib/format";
import type { MessageRecord } from "@/lib/types"; import type { MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useWebSocket } from "@/lib/ws/context"; import { useWebSocket } from "@/lib/ws/context";
@@ -13,6 +14,7 @@ interface LiveMessage {
channelName?: string; channelName?: string;
timestamp: string; timestamp: string;
flagged?: boolean; flagged?: boolean;
metadata?: string | null;
} }
export function LiveStream() { export function LiveStream() {
@@ -40,6 +42,7 @@ export function LiveStream() {
channelName, channelName,
timestamp: new Date().toLocaleTimeString(), timestamp: new Date().toLocaleTimeString(),
flagged: data.ai_status === "flagged" || data.ai_status === "warn", flagged: data.ai_status === "flagged" || data.ai_status === "warn",
metadata: data.metadata ?? null,
}; };
setMessages((prev) => [msg, ...prev].slice(0, 50)); setMessages((prev) => [msg, ...prev].slice(0, 50));
}); });
@@ -80,7 +83,7 @@ export function LiveStream() {
{msg.username} {msg.username}
</span> </span>
<span className="text-xs text-text-secondary truncate flex-1"> <span className="text-xs text-text-secondary truncate flex-1">
{msg.content} {renderMessageContent(msg.content, msg.metadata)}
</span> </span>
<span className="text-[10px] text-text-secondary/40 shrink-0"> <span className="text-[10px] text-text-secondary/40 shrink-0">
{msg.timestamp} {msg.timestamp}
@@ -2,6 +2,7 @@
import { AlertCircle, Check, Trash2 } from "lucide-react"; import { AlertCircle, Check, Trash2 } from "lucide-react";
import { GlassCard } from "@/components/glass/card"; import { GlassCard } from "@/components/glass/card";
import { renderMessageContent } from "@/lib/format";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export interface ModQueueItem { export interface ModQueueItem {
@@ -10,6 +11,7 @@ export interface ModQueueItem {
username: string; username: string;
severity: "low" | "medium" | "high" | "critical"; severity: "low" | "medium" | "high" | "critical";
reason: string; reason: string;
metadata?: string | null;
} }
export function ModQueue({ items = [] }: { items?: ModQueueItem[] }) { export function ModQueue({ items = [] }: { items?: ModQueueItem[] }) {
@@ -56,7 +58,7 @@ export function ModQueue({ items = [] }: { items?: ModQueueItem[] }) {
</span> </span>
</div> </div>
<p className="text-xs text-text-secondary line-clamp-1"> <p className="text-xs text-text-secondary line-clamp-1">
{item.content} {renderMessageContent(item.content, item.metadata)}
</p> </p>
<p className="text-[10px] text-text-secondary/50"> <p className="text-[10px] text-text-secondary/50">
{item.reason} {item.reason}
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useUserDetail, useUsers } from "@/hooks"; import { useUserDetail, useUsers } from "@/hooks";
import { renderMessageContent } from "@/lib/format";
import type { DashboardUser } from "@/lib/types"; import type { DashboardUser } from "@/lib/types";
export function UsersSection() { export function UsersSection() {
@@ -134,7 +135,7 @@ export function UsersSection() {
className="rounded-lg border border-border/40 bg-card/40 px-3 py-2" className="rounded-lg border border-border/40 bg-card/40 px-3 py-2"
> >
<p className="text-xs leading-relaxed text-text-secondary line-clamp-2"> <p className="text-xs leading-relaxed text-text-secondary line-clamp-2">
{msg.content || "(no text content)"} {renderMessageContent(msg.content, msg.metadata) || "(no text content)"}
</p> </p>
<p className="mt-1 text-[10px] font-mono text-text-secondary/40"> <p className="mt-1 text-[10px] font-mono text-text-secondary/40">
{msg.channel_id?.slice(0, 8)} ·{" "} {msg.channel_id?.slice(0, 8)} ·{" "}
@@ -6,7 +6,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { safeParseJsonArray, getMessageChannelLabel } from "@/lib/format"; import { safeParseJsonArray, getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { MessageRecord } from "@/lib/types"; import type { MessageRecord } from "@/lib/types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AiStatusBadge } from "./ai-status-badge"; import { AiStatusBadge } from "./ai-status-badge";
@@ -96,7 +96,7 @@ export function MessageCard({
"italic text-muted-foreground line-through", "italic text-muted-foreground line-through",
)} )}
> >
{msg.content} {renderMessageContent(msg.content, msg.metadata)}
</p> </p>
{(() => { {(() => {
const u = extractFirstImage(msg.metadata); const u = extractFirstImage(msg.metadata);
@@ -4,7 +4,7 @@ import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react";
import { GlassCard } from "@/components/glass/card"; import { GlassCard } from "@/components/glass/card";
import { AttachmentsGrid } from "./attachments-grid"; import { AttachmentsGrid } from "./attachments-grid";
import { AiAnalysisPanel } from "./ai-analysis-panel"; import { AiAnalysisPanel } from "./ai-analysis-panel";
import { getMessageChannelLabel } from "@/lib/format"; import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { AttachmentRecord, MessageRecord } from "@/lib/types"; import type { AttachmentRecord, MessageRecord } from "@/lib/types";
interface MessageDetailViewProps { interface MessageDetailViewProps {
@@ -34,7 +34,7 @@ export function MessageDetailView({ message, attachments, onBack }: MessageDetai
{/* Content */} {/* Content */}
<div className="text-sm text-text-primary/90 leading-relaxed mb-4 whitespace-pre-wrap"> <div className="text-sm text-text-primary/90 leading-relaxed mb-4 whitespace-pre-wrap">
{message.content || "(no text content)"} {renderMessageContent(message.content, message.metadata) || "(no text content)"}
</div> </div>
{/* Attachments */} {/* Attachments */}
@@ -4,7 +4,7 @@ import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react";
import { GlassCard } from "@/components/glass/card"; import { GlassCard } from "@/components/glass/card";
import { AttachmentsGrid } from "./attachments-grid"; import { AttachmentsGrid } from "./attachments-grid";
import { AiAnalysisPanel } from "./ai-analysis-panel"; import { AiAnalysisPanel } from "./ai-analysis-panel";
import { getMessageChannelLabel } from "@/lib/format"; import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { AttachmentRecord, MessageRecord } from "@/lib/types"; import type { AttachmentRecord, MessageRecord } from "@/lib/types";
interface MessageDetailProps { interface MessageDetailProps {
@@ -34,7 +34,7 @@ export function MessageDetail({ message, attachments, onBack }: MessageDetailPro
{/* Content */} {/* Content */}
<div className="text-sm text-text-primary/90 leading-relaxed mb-4 whitespace-pre-wrap"> <div className="text-sm text-text-primary/90 leading-relaxed mb-4 whitespace-pre-wrap">
{message.content || "(no text content)"} {renderMessageContent(message.content, message.metadata) || "(no text content)"}
</div> </div>
{/* Attachments */} {/* Attachments */}
@@ -4,7 +4,7 @@ import { Search, X } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { messagesApi } from "@/lib/api"; import { messagesApi } from "@/lib/api";
import { getMessageChannelLabel } from "@/lib/format"; import { getMessageChannelLabel, renderMessageContent } from "@/lib/format";
import type { MessageRecord } from "@/lib/types"; import type { MessageRecord } from "@/lib/types";
interface SearchOverlayProps { interface SearchOverlayProps {
@@ -86,7 +86,7 @@ export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) {
<span className="font-medium text-text-primary">{msg.username}</span> <span className="font-medium text-text-primary">{msg.username}</span>
<span className="text-text-secondary/40">{getMessageChannelLabel(msg)}</span> <span className="text-text-secondary/40">{getMessageChannelLabel(msg)}</span>
</div> </div>
<p className="text-xs text-text-secondary/80 line-clamp-1 mt-0.5">{msg.content}</p> <p className="text-xs text-text-secondary/80 line-clamp-1 mt-0.5">{renderMessageContent(msg.content, msg.metadata)}</p>
</button> </button>
)) ))
)} )}
+64
View File
@@ -1,3 +1,5 @@
import type { MessageMetadata } from "@/lib/types/message";
/** /**
* Format a number with locale separators. * Format a number with locale separators.
*/ */
@@ -72,3 +74,65 @@ export function getMessageChannelLabel(msg: {
if (channelName) return channelName; if (channelName) return channelName;
return msg.channel_id?.slice(0, 8) ?? ""; 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; 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 { export interface AttachmentRef {
name: string; name: string;
url: string; url: string;
@@ -70,6 +87,9 @@ export interface MessageMetadata {
stickers?: StickerInfo[] | null; stickers?: StickerInfo[] | null;
attachments?: AttachmentRef[] | null; attachments?: AttachmentRef[] | null;
embeds?: EmbedInfo[] | null; embeds?: EmbedInfo[] | null;
customEmojis?: CustomEmojiInfo[] | null;
mentionedRoles?: MentionedRoleInfo[] | null;
mentionedUsers?: MentionedUserInfo[] | null;
channel?: ChannelRef | null; channel?: ChannelRef | null;
reference?: ReferenceInfo | null; reference?: ReferenceInfo | null;
} }