From aaf3b2aa2f95e30b14db5618d4c7c1ab004a8356 Mon Sep 17 00:00:00 2001 From: Fisto Date: Wed, 16 Sep 2026 07:48:21 +0200 Subject: [PATCH] Add embeds, forwards, and voice flags to the message shape --- CURRENT.md | 2 +- docs/core-bridge.md | 2 +- src/renderer/puppet/actions.ts | 69 ++++++++++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/CURRENT.md b/CURRENT.md index 4788ca2..017ab9b 100644 --- a/CURRENT.md +++ b/CURRENT.md @@ -23,7 +23,7 @@ mobile draft at 390px. Checkpoints, push after each: - [x] C1 design docs, AGENTS/INDEX pointers, `.gitignore` - - [ ] C2 core message shape: embeds, forwards, attachment metadata + - [x] C2 core message shape: embeds, forwards, attachment metadata - [ ] C3 media-scrape store and puppet walker, tests - [ ] C4 slopbot backend, tests - [ ] C5 Web UI Media and Bot tabs, API routes diff --git a/docs/core-bridge.md b/docs/core-bridge.md index a199e40..b00fcbc 100644 --- a/docs/core-bridge.md +++ b/docs/core-bridge.md @@ -67,7 +67,7 @@ Event line: | `user.search` | `query`, optional `guildId`, optional `limit` | Users from the client cache. Guild members when `guildId` is set. | | `dm.list` | optional `limit` | Cached DM and group DM channels with recipients, icons, and avatars. Cache-only. | | `message.send` | `channelId`, `content` | `{ channelId, messageId, sent: true }` | -| `message.read` | `channelId`, `limit` | `{ cached, messages }`. Cache-only. Messages include the author avatar, attachment CDN links, `editedAt`, and `replyTo`. | +| `message.read` | `channelId`, `limit` | `{ cached, messages }`. Cache-only. Messages carry the author avatar, attachment metadata (id, filename, URL, content type, size, dimensions, duration), embeds (video, image, and thumbnail URLs), sticker items, forwarded snapshots, a voice-message flag, `editedAt`, and `replyTo`. | | `message.fetch` | `channelId`, `limit`, optional `before` or `after` | `{ fetched, messages }`. Fetches from Discord. Same message shape. | | `reaction.add` | `channelId`, `messageId`, `emoji` | `{ added: true }` | | `dm.send` | `userId`, `content` | `{ userId, channelId, sent: true }` | diff --git a/src/renderer/puppet/actions.ts b/src/renderer/puppet/actions.ts index ccc9843..969da37 100644 --- a/src/renderer/puppet/actions.ts +++ b/src/renderer/puppet/actions.ts @@ -34,6 +34,9 @@ const SENDABLE_CHANNEL_TYPES = new Set([0, 1, 3, 5, 10, 11, 12]); /** Channel types that are voice channels. */ const VOICE_CHANNEL_TYPES = new Set([2, 13]); +/** Discord message flag for a voice message. */ +const VOICE_MESSAGE_FLAG = 1 << 13; + /** Discord's message fetch action, located lazily at runtime. */ const MessageActions = findByPropsLazy("fetchMessages"); @@ -188,10 +191,64 @@ function getSendableChannel(channelId: string) { return channel; } +/** Pull a URL from an embed part. Discord gives some parts as strings and some as objects. */ +function embedUrl(value: any): string | null { + if (typeof value === "string") return value; + return value?.url ?? value?.proxyURL ?? value?.proxy_url ?? null; +} + +function mapAttachments(message: any) { + return (message?.attachments ?? []).map((attachment: any) => ({ + id: attachment.id ?? null, + filename: attachment.filename, + url: attachment.url ?? null, + contentType: attachment.contentType ?? attachment.content_type ?? null, + size: attachment.size ?? null, + width: attachment.width ?? null, + height: attachment.height ?? null, + durationSecs: attachment.durationSecs ?? attachment.duration_secs ?? null + })); +} + +function mapEmbeds(message: any) { + return (message?.embeds ?? []).map((embed: any) => ({ + type: embed?.type ?? null, + url: embed?.url ?? null, + title: embed?.title ?? null, + provider: embed?.provider?.name ?? null, + videoUrl: embedUrl(embed?.video), + imageUrl: embedUrl(embed?.image), + thumbnailUrl: embedUrl(embed?.thumbnail) + })); +} + +function mapStickers(message: any) { + return (message?.stickerItems ?? message?.sticker_items ?? []).map((sticker: any) => ({ + id: sticker.id ?? null, + name: sticker.name ?? null, + formatType: sticker.formatType ?? sticker.format_type ?? null + })); +} + +/** Shape a forwarded snapshot. Snapshots carry no channel fields and are one level deep. */ +function mapSnapshot(snapshot: any) { + const message = snapshot?.message ?? snapshot; + + return { + content: message?.content ?? "", + timestamp: message?.timestamp ?? null, + attachments: mapAttachments(message), + embeds: mapEmbeds(message), + stickerItems: mapStickers(message), + voiceMessage: Boolean((message?.flags ?? 0) & VOICE_MESSAGE_FLAG) + }; +} + /** Shape a Discord message for the daemon. Attachments carry CDN links only. */ function mapMessage(message: any) { const reference = message.messageReference; const referenced = message.referencedMessage ?? message.referenced_message; + const snapshots = message.messageSnapshots ?? message.message_snapshots ?? []; return { id: message.id, @@ -210,13 +267,11 @@ function mapMessage(message: any) { authorName: referenced?.author?.username ?? null } : null, - attachments: (message.attachments ?? []).map((attachment: any) => ({ - filename: attachment.filename, - url: attachment.url ?? null, - contentType: attachment.contentType ?? null, - width: attachment.width ?? null, - height: attachment.height ?? null - })) + attachments: mapAttachments(message), + embeds: mapEmbeds(message), + stickerItems: mapStickers(message), + forwarded: snapshots.map(mapSnapshot), + voiceMessage: Boolean((message.flags ?? 0) & VOICE_MESSAGE_FLAG) }; }