diff --git a/services/discord-gateway/src/modules/message-capture/messageMetadata.ts b/services/discord-gateway/src/modules/message-capture/messageMetadata.ts index aa157ec..fa81912 100644 --- a/services/discord-gateway/src/modules/message-capture/messageMetadata.ts +++ b/services/discord-gateway/src/modules/message-capture/messageMetadata.ts @@ -82,6 +82,9 @@ export interface RichMessageMetadata { channelId: string | null; guildId: string | null; type: string | null; + content: string | null; + repliedUsername: string | null; + repliedUserId: string | null; } | null; isCrosspost: boolean; } @@ -207,8 +210,39 @@ export function getEmbedMetadata( })); } +/** + * Try to get referenced message content from the channel cache. + * For replies, Discord sends `referenced_message` in the API, which + * discord.js-selfbot-v13 caches in the channel's message manager. + * Returns null if the message isn't cached (e.g. forwards without + * referenced_message payload). + */ +function getReferencedMessageContent( + message: Message, +): { content: string; username: string; userId: string } | null { + const ref = message.reference; + if (!ref?.messageId) return null; + try { + const cached = (message.channel as any)?.messages?.cache?.get( + ref.messageId, + ); + if (cached?.content) { + return { + content: cached.content, + username: cached.author?.username ?? "Unknown", + userId: cached.author?.id ?? "", + }; + } + } catch { + // Cache may not be available or message not in it + } + return null; +} + export function getMessageMetadata(message: Message): RichMessageMetadata { const member = message.member; + const referenceContent = getReferencedMessageContent(message); + const ref = message.reference; return { stickers: getStickerMetadata(message), embeds: getEmbedMetadata(message), @@ -232,13 +266,16 @@ export function getMessageMetadata(message: Message): RichMessageMetadata { } : null, channel: getMessageLocation(message), - reference: message.reference + reference: ref ? { - messageId: message.reference.messageId ?? null, - channelId: message.reference.channelId ?? null, - guildId: message.reference.guildId ?? null, + messageId: ref.messageId ?? null, + channelId: ref.channelId ?? null, + guildId: ref.guildId ?? null, type: - (message.reference.type as unknown as string | undefined) ?? null, + (ref.type as unknown as string | undefined) ?? null, + content: referenceContent?.content ?? null, + repliedUsername: referenceContent?.username ?? null, + repliedUserId: referenceContent?.userId ?? null, } : null, isCrosspost: message.flags?.has(1 << 1) ?? false, diff --git a/services/frontend/src/features/messages/components/MessageCard.tsx b/services/frontend/src/features/messages/components/MessageCard.tsx index 9f63719..b7ad6e7 100644 --- a/services/frontend/src/features/messages/components/MessageCard.tsx +++ b/services/frontend/src/features/messages/components/MessageCard.tsx @@ -1,16 +1,20 @@ import { AlertCircle, CheckCircle2, + Forward, Hash, Image as ImageIcon, + MessageCircle, Pencil, + Reply, RotateCw, Smile, Trash2, } from "lucide-react"; -import { Fragment, useMemo, useState } from "react"; +import { Fragment, useEffect, useMemo, useState } from "react"; import type { MessageRecord } from "../../../entities/message/types.js"; import { parseMetadata } from "../../../shared/lib/utils.js"; +import { getMessageById } from "../../../shared/api/client.js"; import { Badge, Button, Skeleton, StatusBadge } from "../../../shared/ui"; const CUSTOM_EMOJI_REGEX = /<(a)?:([a-zA-Z0-9_]+):(\d+)>/g; @@ -127,6 +131,35 @@ function MessageRow({ message.ai_confidence ?? message.ai_moderation_score ?? null; const [isReanalyzing, setIsReanalyzing] = useState(false); + // ── Fetch referenced message content for replies if not in metadata ── + const referenceMeta = metadata.reference; + const [fetchedRefContent, setFetchedRefContent] = useState<{ + username: string; + content: string; + } | null>(null); + + useEffect(() => { + if ( + message.is_reply && + referenceMeta?.messageId && + !referenceMeta?.content && + !message.deleted_at + ) { + getMessageById(referenceMeta.messageId) + .then((refMsg) => { + if (refMsg) { + setFetchedRefContent({ + username: refMsg.username, + content: refMsg.content, + }); + } + }) + .catch(() => { + // Referenced message might not exist in our DB + }); + } + }, [message.is_reply, referenceMeta?.messageId, referenceMeta?.content, message.deleted_at]); + const analysisSummary = useMemo(() => { const parts: string[] = []; if (categories.length > 0) { @@ -167,6 +200,58 @@ function MessageRow({ } }; + // ── Reference context (reply / forward / crosspost) ───────────────── + const renderReferenceIndicator = () => { + // Use fetched content if metadata doesn't have it + const effectiveRepliedUsername = + referenceMeta?.repliedUsername ?? fetchedRefContent?.username ?? null; + const effectiveRepliedContent = + referenceMeta?.content ?? fetchedRefContent?.content ?? null; + + if (message.is_reply) { + return ( +
+ + + + Replying to{" "} + {effectiveRepliedUsername + ? `@${effectiveRepliedUsername}` + : "a message"} + + {effectiveRepliedContent && ( + + {effectiveRepliedContent} + + )} + +
+ ); + } + + if (message.is_forward) { + return ( +
+ + Forwarded +
+ ); + } + + if (message.is_crosspost) { + return ( +
+ + Crossposted +
+ ); + } + + return null; + }; + + const referenceIndicator = renderReferenceIndicator(); + return (
{/* Row header: time + edit/delete indicators + AI badges */} @@ -211,6 +296,9 @@ function MessageRow({
+ {/* Reference context: reply / forward / crosspost */} + {referenceIndicator} + {/* Content — hidden when it's just an "[Attachment: ...]" fallback and the image is shown below */} {shouldShowContent ? (

{ return request(`/api/messages/${id}/reanalyze`, { method: "POST" }); } +export function getMessageById( + id: string, +): Promise { + return request(`/api/messages/detail/${id}`); +} + export function reanalyzeErrorBatch(opts: { guildId?: string; channelId?: string; diff --git a/services/frontend/src/shared/lib/utils.ts b/services/frontend/src/shared/lib/utils.ts index ab20ca6..cf8ff05 100644 --- a/services/frontend/src/shared/lib/utils.ts +++ b/services/frontend/src/shared/lib/utils.ts @@ -27,6 +27,16 @@ export interface MessageMetadata { threadId?: string; threadName?: string; }; + reference?: { + messageId: string | null; + channelId: string | null; + guildId: string | null; + type: string | null; + content: string | null; + repliedUsername: string | null; + repliedUserId: string | null; + } | null; + isCrosspost?: boolean; } export function parseMetadata(value: string | null): MessageMetadata {