From b4a0dbc74d48fadb9b886c5063cffc10faa60828 Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:00:07 +0200 Subject: [PATCH] fix(messaging): preserve group inbound media URLs in fan-out (#66) Group channel fan-out used event.content only, dropping mediaUrl that DM handling already merges into message text. Photos with empty captions in SendBlue groups reached member bots with blank prompts and blocks. Also skip fan-out when the merged text is empty, matching DM tapback behavior. Co-authored-by: Cursor Agent Co-authored-by: cynarAI --- apps/api/src/messaging-inbound.test.ts | 30 ++++++++++++++++++++++++++ apps/api/src/messaging-inbound.ts | 8 +++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/api/src/messaging-inbound.test.ts b/apps/api/src/messaging-inbound.test.ts index 59f2f97..3e32d3f 100644 --- a/apps/api/src/messaging-inbound.test.ts +++ b/apps/api/src/messaging-inbound.test.ts @@ -911,6 +911,36 @@ describe("createMessagingInboundHandler channel routing", () => { expect(runJobs).toHaveLength(2); }); + it("appends inbound media links to group fan-out text and prompt", async () => { + const senderMember = { + id: "cm-1", + channelId: "ch-1", + address: "+15551111111", + identityId: "mi-1", + status: "approved", + }; + const deps = createDeps({ members: [senderMember] }); + const handle = createMessagingInboundHandler(deps); + + await handle({ + ...groupEvent, + content: "", + mediaUrl: "https://cdn.example.com/group-photo.jpg", + }); + + expect(deps.sendUserMessage).toHaveBeenCalledWith( + expect.objectContaining({ + prompt: `[Group "Family", Alice]: https://cdn.example.com/group-photo.jpg`, + blocks: [ + expect.objectContaining({ + kind: "channel_message", + text: "https://cdn.example.com/group-photo.jpg", + }), + ], + }), + ); + }); + it("wakes matching routines only after a channel member is approved", async () => { const approved = { id: "cm-1", diff --git a/apps/api/src/messaging-inbound.ts b/apps/api/src/messaging-inbound.ts index 673b026..325b905 100644 --- a/apps/api/src/messaging-inbound.ts +++ b/apps/api/src/messaging-inbound.ts @@ -581,6 +581,10 @@ async function handleChannelEvent( const approved = await deps.prisma.messagingChannelMember.findMany({ where: { channelId: channel.id, status: "approved", identityId: { not: null } }, }); + // Same v1 media-as-text merge as DMs: group photos often arrive with an + // empty caption and only a CDN URL. + const text = [event.content, event.mediaUrl].filter(Boolean).join("\n"); + if (!text) return; const block: MessageBlock = { kind: "channel_message", provider: event.provider, @@ -588,10 +592,10 @@ async function handleChannelEvent( channelId: channel.id, fromAddress: event.from, fromLabel, - text: event.content, + text, hop: 0, }; - const prompt = `[Group "${channel.name ?? "group"}", ${fromLabel}]: ${event.content}`; + const prompt = `[Group "${channel.name ?? "group"}", ${fromLabel}]: ${text}`; for (const member of approved) { const identity = await deps.prisma.messagingIdentity.findUnique({ where: { id: member.identityId! },