From e5514d4854ed4d8e4e9d5df861d8a54832cb2143 Mon Sep 17 00:00:00 2001 From: Tyler Yust Date: Tue, 20 Jan 2026 01:25:42 -0800 Subject: [PATCH] feat: implement reply context handling in BlueBubbles messaging, enhancing message formatting and metadata resolution --- ...essages-without-mention-by-default.test.ts | 39 +++++++++++++++++++ src/imessage/monitor/monitor-provider.ts | 34 +++++++++++++++- src/imessage/monitor/types.ts | 3 ++ src/imessage/send.test.ts | 7 ++++ src/imessage/send.ts | 17 +++++++- 5 files changed, 97 insertions(+), 3 deletions(-) diff --git a/src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts b/src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts index 31af2eb8d..a0665044e 100644 --- a/src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts +++ b/src/imessage/monitor.skips-group-messages-without-mention-by-default.test.ts @@ -495,4 +495,43 @@ describe("monitorIMessageProvider", () => { expect(body).toContain("Test Group id:99"); expect(body).toContain("+15550001111: @clawd hi"); }); + + it("includes reply context when imessage reply metadata is present", async () => { + const run = monitorIMessageProvider(); + await waitForSubscribe(); + + notificationHandler?.({ + method: "message", + params: { + message: { + id: 12, + chat_id: 55, + sender: "+15550001111", + is_from_me: false, + text: "replying now", + is_group: false, + reply_to_id: 9001, + reply_to_text: "original message", + reply_to_sender: "+15559998888", + }, + }, + }); + + await flush(); + closeResolve?.(); + await run; + + expect(replyMock).toHaveBeenCalled(); + const ctx = replyMock.mock.calls[0]?.[0] as { + Body?: string; + ReplyToId?: string; + ReplyToBody?: string; + ReplyToSender?: string; + }; + expect(ctx.ReplyToId).toBe("9001"); + expect(ctx.ReplyToBody).toBe("original message"); + expect(ctx.ReplyToSender).toBe("+15559998888"); + expect(String(ctx.Body ?? "")).toContain("[Replying to +15559998888 id:9001]"); + expect(String(ctx.Body ?? "")).toContain("original message"); + }); }); diff --git a/src/imessage/monitor/monitor-provider.ts b/src/imessage/monitor/monitor-provider.ts index 2e65848ac..65185087b 100644 --- a/src/imessage/monitor/monitor-provider.ts +++ b/src/imessage/monitor/monitor-provider.ts @@ -92,6 +92,29 @@ async function detectRemoteHostFromCliPath(cliPath: string): Promise { const runtime = resolveRuntime(opts); const cfg = opts.config ?? loadConfig(); @@ -324,6 +347,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P const placeholder = kind ? `` : attachments?.length ? "" : ""; const bodyText = messageText || placeholder; if (!bodyText) return; + const replyContext = describeReplyContext(message); const createdAt = message.created_at ? Date.parse(message.created_at) : undefined; const historyKey = isGroup ? String(chatId ?? chatGuid ?? chatIdentifier ?? "unknown") @@ -414,11 +438,16 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P storePath, sessionKey: route.sessionKey, }); + const replySuffix = replyContext + ? `\n\n[Replying to ${replyContext.sender ?? "unknown sender"}${ + replyContext.id ? ` id:${replyContext.id}` : "" + }]\n${replyContext.body}\n[/Replying]` + : ""; const body = formatInboundEnvelope({ channel: "iMessage", from: fromLabel, timestamp: createdAt, - body: bodyText, + body: `${bodyText}${replySuffix}`, chatType: isGroup ? "group" : "direct", sender: { name: senderNormalized, id: sender }, previousTimestamp, @@ -462,6 +491,9 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P Provider: "imessage", Surface: "imessage", MessageSid: message.id ? String(message.id) : undefined, + ReplyToId: replyContext?.id, + ReplyToBody: replyContext?.body, + ReplyToSender: replyContext?.sender, Timestamp: createdAt, MediaPath: mediaPath, MediaType: mediaType, diff --git a/src/imessage/monitor/types.ts b/src/imessage/monitor/types.ts index 584475fc6..7fc00c7fd 100644 --- a/src/imessage/monitor/types.ts +++ b/src/imessage/monitor/types.ts @@ -13,6 +13,9 @@ export type IMessagePayload = { sender?: string | null; is_from_me?: boolean | null; text?: string | null; + reply_to_id?: number | string | null; + reply_to_text?: string | null; + reply_to_sender?: string | null; created_at?: string | null; attachments?: IMessageAttachment[] | null; chat_identifier?: string | null; diff --git a/src/imessage/send.test.ts b/src/imessage/send.test.ts index 318c5d713..47c460471 100644 --- a/src/imessage/send.test.ts +++ b/src/imessage/send.test.ts @@ -65,4 +65,11 @@ describe("sendMessageIMessage", () => { expect(params.file).toBe("/tmp/imessage-media.jpg"); expect(params.text).toBe(""); }); + + it("returns message id when rpc provides one", async () => { + requestMock.mockResolvedValue({ ok: true, id: 123 }); + const { sendMessageIMessage } = await loadSendMessageIMessage(); + const result = await sendMessageIMessage("chat_id:7", "hello"); + expect(result.messageId).toBe("123"); + }); }); diff --git a/src/imessage/send.ts b/src/imessage/send.ts index 459ffe34e..32e963bc8 100644 --- a/src/imessage/send.ts +++ b/src/imessage/send.ts @@ -23,6 +23,18 @@ export type IMessageSendResult = { messageId: string; }; +function resolveMessageId(result: Record | null | undefined): string | null { + if (!result) return null; + const raw = + (typeof result.messageId === "string" && result.messageId.trim()) || + (typeof result.message_id === "string" && result.message_id.trim()) || + (typeof result.id === "string" && result.id.trim()) || + (typeof result.guid === "string" && result.guid.trim()) || + (typeof result.message_id === "number" ? String(result.message_id) : null) || + (typeof result.id === "number" ? String(result.id) : null); + return raw ? String(raw).trim() : null; +} + async function resolveAttachment( mediaUrl: string, maxBytes: number, @@ -97,11 +109,12 @@ export async function sendMessageIMessage( const client = opts.client ?? (await createIMessageRpcClient({ cliPath, dbPath })); const shouldClose = !opts.client; try { - const result = await client.request<{ ok?: boolean }>("send", params, { + const result = await client.request>("send", params, { timeoutMs: opts.timeoutMs, }); + const resolvedId = resolveMessageId(result); return { - messageId: result?.ok ? "ok" : "unknown", + messageId: resolvedId ?? (result?.ok ? "ok" : "unknown"), }; } finally { if (shouldClose) {