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 ( +
{
return request