From 20bc89d96c33997c637284baf8d4a3ae6c65642e Mon Sep 17 00:00:00 2001 From: Tyler Yust Date: Tue, 20 Jan 2026 01:14:40 -0800 Subject: [PATCH] feat: enhance BlueBubbles messaging targets by adding support for UUID and hex chat identifiers, improving normalization and parsing functions --- extensions/bluebubbles/src/monitor.test.ts | 6 +++- extensions/bluebubbles/src/monitor.ts | 21 +++---------- extensions/bluebubbles/src/targets.test.ts | 36 ++++++++++++++++++++++ extensions/bluebubbles/src/targets.ts | 21 +++++++++++++ 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index ad859a3c5..96592edbe 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -1336,13 +1336,17 @@ describe("BlueBubbles webhook monitor", () => { }, }; + mockDispatchReplyWithBufferedBlockDispatcher.mockImplementationOnce(async (params) => { + await params.dispatcherOptions.onReplyStart?.(); + }); + const req = createMockRequest("POST", "/bluebubbles-webhook", payload); const res = createMockResponse(); await handleBlueBubblesWebhookRequest(req, res); await new Promise((resolve) => setTimeout(resolve, 50)); - // Should call typing start + // Should call typing start when reply flow triggers it. expect(sendBlueBubblesTyping).toHaveBeenCalledWith( expect.any(String), true, diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index d3e87f025..9936950e9 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -45,23 +45,22 @@ function logGroupAllowlistHint(params: { chatName?: string; accountId?: string; }): void { - const logger = params.runtime.log; - if (!logger) return; + const log = params.runtime.log ?? console.log; const nameHint = params.chatName ? ` (group name: ${params.chatName})` : ""; const accountHint = params.accountId ? ` (or channels.bluebubbles.accounts.${params.accountId}.groupAllowFrom)` : ""; if (params.entry) { - logger( + log( `[bluebubbles] group message blocked (${params.reason}). Allow this group by adding ` + `"${params.entry}" to channels.bluebubbles.groupAllowFrom${nameHint}.`, ); - logger( + log( `[bluebubbles] add to config: channels.bluebubbles.groupAllowFrom=["${params.entry}"]${accountHint}.`, ); return; } - logger( + log( `[bluebubbles] group message blocked (${params.reason}). Allow groups by setting ` + `channels.bluebubbles.groupPolicy="open" or adding a group id to ` + `channels.bluebubbles.groupAllowFrom${accountHint}${nameHint}.`, @@ -1329,18 +1328,6 @@ async function processMessage( }; let sentMessage = false; - if (chatGuidForActions && baseUrl && password) { - logVerbose(core, runtime, `typing start (pre-dispatch) chatGuid=${chatGuidForActions}`); - try { - await sendBlueBubblesTyping(chatGuidForActions, true, { - cfg: config, - accountId: account.accountId, - }); - } catch (err) { - runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`); - } - } - try { await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, diff --git a/extensions/bluebubbles/src/targets.test.ts b/extensions/bluebubbles/src/targets.test.ts index 731d6dbf6..ae2851ef9 100644 --- a/extensions/bluebubbles/src/targets.test.ts +++ b/extensions/bluebubbles/src/targets.test.ts @@ -57,6 +57,15 @@ describe("normalizeBlueBubblesMessagingTarget", () => { expect(normalizeBlueBubblesMessagingTarget("chat123")).toBe("chat_identifier:chat123"); expect(normalizeBlueBubblesMessagingTarget("Chat456789")).toBe("chat_identifier:Chat456789"); }); + + it("normalizes UUID/hex chat identifiers", () => { + expect(normalizeBlueBubblesMessagingTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toBe( + "chat_identifier:8b9c1a10536d4d86a336ea03ab7151cc", + ); + expect(normalizeBlueBubblesMessagingTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe( + "chat_identifier:1C2D3E4F-1234-5678-9ABC-DEF012345678", + ); + }); }); describe("looksLikeBlueBubblesTargetId", () => { @@ -82,6 +91,11 @@ describe("looksLikeBlueBubblesTargetId", () => { expect(looksLikeBlueBubblesTargetId("Chat456789")).toBe(true); }); + it("accepts UUID/hex chat identifiers", () => { + expect(looksLikeBlueBubblesTargetId("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(true); + expect(looksLikeBlueBubblesTargetId("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(true); + }); + it("rejects display names", () => { expect(looksLikeBlueBubblesTargetId("Jane Doe")).toBe(false); }); @@ -103,6 +117,17 @@ describe("parseBlueBubblesTarget", () => { }); }); + it("parses UUID/hex chat identifiers as chat_identifier", () => { + expect(parseBlueBubblesTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({ + kind: "chat_identifier", + chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc", + }); + expect(parseBlueBubblesTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({ + kind: "chat_identifier", + chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678", + }); + }); + it("parses explicit chat_id: prefix", () => { expect(parseBlueBubblesTarget("chat_id:123")).toEqual({ kind: "chat_id", chatId: 123 }); }); @@ -135,6 +160,17 @@ describe("parseBlueBubblesAllowTarget", () => { }); }); + it("parses UUID/hex chat identifiers as chat_identifier", () => { + expect(parseBlueBubblesAllowTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({ + kind: "chat_identifier", + chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc", + }); + expect(parseBlueBubblesAllowTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({ + kind: "chat_identifier", + chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678", + }); + }); + it("parses explicit chat_id: prefix", () => { expect(parseBlueBubblesAllowTarget("chat_id:456")).toEqual({ kind: "chat_id", chatId: 456 }); }); diff --git a/extensions/bluebubbles/src/targets.ts b/extensions/bluebubbles/src/targets.ts index a51415416..94320592c 100644 --- a/extensions/bluebubbles/src/targets.ts +++ b/extensions/bluebubbles/src/targets.ts @@ -20,6 +20,9 @@ const SERVICE_PREFIXES: Array<{ prefix: string; service: BlueBubblesService }> = { prefix: "sms:", service: "sms" }, { prefix: "auto:", service: "auto" }, ]; +const CHAT_IDENTIFIER_UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const CHAT_IDENTIFIER_HEX_RE = /^[0-9a-f]{24,64}$/i; function parseRawChatGuid(value: string): string | null { const trimmed = value.trim(); @@ -45,6 +48,13 @@ function stripBlueBubblesPrefix(value: string): string { return trimmed.slice("bluebubbles:".length).trim(); } +function looksLikeRawChatIdentifier(value: string): boolean { + const trimmed = value.trim(); + if (!trimmed) return false; + if (/^chat\d+$/i.test(trimmed)) return true; + return CHAT_IDENTIFIER_UUID_RE.test(trimmed) || CHAT_IDENTIFIER_HEX_RE.test(trimmed); +} + export function normalizeBlueBubblesHandle(raw: string): string { const trimmed = raw.trim(); if (!trimmed) return ""; @@ -113,6 +123,7 @@ export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): } // Recognize chat patterns (e.g., "chat660250192681427962") as chat IDs if (/^chat\d+$/i.test(candidate)) return true; + if (looksLikeRawChatIdentifier(candidate)) return true; if (candidate.includes("@")) return true; const digitsOnly = candidate.replace(/[\s().-]/g, ""); if (/^\+?\d{3,}$/.test(digitsOnly)) return true; @@ -200,6 +211,11 @@ export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { return { kind: "chat_identifier", chatIdentifier: trimmed }; } + // Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc") + if (looksLikeRawChatIdentifier(trimmed)) { + return { kind: "chat_identifier", chatIdentifier: trimmed }; + } + return { kind: "handle", to: trimmed, service: "auto" }; } @@ -251,6 +267,11 @@ export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget return { kind: "chat_identifier", chatIdentifier: trimmed }; } + // Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc") + if (looksLikeRawChatIdentifier(trimmed)) { + return { kind: "chat_identifier", chatIdentifier: trimmed }; + } + return { kind: "handle", handle: normalizeBlueBubblesHandle(trimmed) }; }