From f997a3f39532eac9e40d981a6ea53a8e5b6110bf Mon Sep 17 00:00:00 2001 From: Tyler Yust Date: Sun, 25 Jan 2026 00:08:47 -0800 Subject: [PATCH] fix(bluebubbles): prevent message routing to group chats when targeting phone numbers When sending a message to a phone number like +12622102921, the resolveChatGuidForTarget function was finding and returning a GROUP CHAT containing that phone number instead of a direct DM chat. The bug was in the participantMatch fallback logic which matched ANY chat containing the phone number as a participant, including groups. This fix adds a check to ensure participantMatch only considers DM chats (identified by ';-;' separator in the chat GUID). Group chats (identified by ';+;' separator) are now explicitly excluded from handle-based matching. If a phone number only exists in a group chat (no direct DM exists), the function now correctly returns null, which causes the send to fail with a clear error rather than accidentally messaging a group. Added test case to verify this behavior. --- extensions/bluebubbles/src/send.test.ts | 41 +++++++++++++++++++++++++ extensions/bluebubbles/src/send.ts | 16 +++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/extensions/bluebubbles/src/send.test.ts b/extensions/bluebubbles/src/send.test.ts index 0b8b77a1f..6509ec3bf 100644 --- a/extensions/bluebubbles/src/send.test.ts +++ b/extensions/bluebubbles/src/send.test.ts @@ -187,6 +187,47 @@ describe("send", () => { expect(result).toBe("iMessage;-;+15551234567"); }); + it("returns null when handle only exists in group chat (not DM)", async () => { + // This is the critical fix: if a phone number only exists as a participant in a group chat + // (no direct DM chat), we should NOT send to that group. Return null instead. + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + data: [ + { + guid: "iMessage;+;group-the-council", + participants: [ + { address: "+12622102921" }, + { address: "+15550001111" }, + { address: "+15550002222" }, + ], + }, + ], + }), + }) + // Empty second page to stop pagination + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ data: [] }), + }); + + const target: BlueBubblesSendTarget = { + kind: "handle", + address: "+12622102921", + service: "imessage", + }; + const result = await resolveChatGuidForTarget({ + baseUrl: "http://localhost:1234", + password: "test", + target, + }); + + // Should return null, NOT the group chat GUID + expect(result).toBeNull(); + }); + it("returns null when chat not found", async () => { mockFetch.mockResolvedValueOnce({ ok: true, diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index 675063d6d..a85ccc4c4 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -257,11 +257,17 @@ export async function resolveChatGuidForTarget(params: { return guid; } if (!participantMatch && guid) { - const participants = extractParticipantAddresses(chat).map((entry) => - normalizeBlueBubblesHandle(entry), - ); - if (participants.includes(normalizedHandle)) { - participantMatch = guid; + // Only consider DM chats (`;-;` separator) as participant matches. + // Group chats (`;+;` separator) should never match when searching by handle/phone. + // This prevents routing "send to +1234567890" to a group chat that contains that number. + const isDmChat = guid.includes(";-;"); + if (isDmChat) { + const participants = extractParticipantAddresses(chat).map((entry) => + normalizeBlueBubblesHandle(entry), + ); + if (participants.includes(normalizedHandle)) { + participantMatch = guid; + } } } }