From 37f12eb7eee30d2f5a76b0b41810dd7922fc982b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 11:47:17 +0100 Subject: [PATCH] fix: align BlueBubbles private-api null fallback + warning (#23459) (thanks @echoVic) --- CHANGELOG.md | 1 + extensions/bluebubbles/src/send.test.ts | 30 +++++++++++++++++++++++++ extensions/bluebubbles/src/send.ts | 11 +++++++++ 3 files changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b420a9112..b810b006527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:]]`, `[[audio_as_voice]]`) from displayed chat history, live chat event output, and session preview snippets so control tags no longer leak into user-visible surfaces. - Telegram/Retry: classify undici `TypeError: fetch failed` as recoverable in both polling and send retry paths so transient fetch failures no longer fail fast. (#16699) thanks @Glucksberg. - BlueBubbles/DM history: restore DM backfill context with account-scoped rolling history, bounded backfill retries, and safer history payload limits. (#20302) Thanks @Ryan-Haines. +- BlueBubbles/Private API cache: treat unknown (`null`) private-API cache status as disabled for send/attachment/reply flows to avoid stale-cache 500s, and log a warning when reply/effect features are requested while capability is unknown. (#23459) Thanks @echoVic. - BlueBubbles/Webhooks: accept inbound/reaction webhook payloads when BlueBubbles omits `handle` but provides DM `chatGuid`, and harden payload extraction for array/string-wrapped message bodies so valid webhook events no longer get rejected as unparseable. (#23275) Thanks @toph31. - Security/Audit: add `openclaw security audit` finding `gateway.nodes.allow_commands_dangerous` for risky `gateway.nodes.allowCommands` overrides, with severity upgraded to critical on remote gateway exposure. - Gateway/Control plane: reduce cross-client write limiter contention by adding `connId` fallback keying when device ID and client IP are both unavailable. diff --git a/extensions/bluebubbles/src/send.test.ts b/extensions/bluebubbles/src/send.test.ts index c1bcafe29cb..7a2edeaf850 100644 --- a/extensions/bluebubbles/src/send.test.ts +++ b/extensions/bluebubbles/src/send.test.ts @@ -527,6 +527,7 @@ describe("send", () => { }); it("uses private-api when reply metadata is present", async () => { + vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(true); mockResolvedHandleTarget(); mockSendResponse({ data: { guid: "msg-uuid-124" } }); @@ -568,6 +569,7 @@ describe("send", () => { }); it("normalizes effect names and uses private-api for effects", async () => { + vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(true); mockResolvedHandleTarget(); mockSendResponse({ data: { guid: "msg-uuid-125" } }); @@ -586,6 +588,34 @@ describe("send", () => { expect(body.effectId).toBe("com.apple.MobileSMS.expressivesend.invisibleink"); }); + it("warns and downgrades private-api features when status is unknown", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + mockResolvedHandleTarget(); + mockSendResponse({ data: { guid: "msg-uuid-unknown" } }); + + try { + const result = await sendMessageBlueBubbles("+15551234567", "Reply fallback", { + serverUrl: "http://localhost:1234", + password: "test", + replyToMessageGuid: "reply-guid-123", + effectId: "invisible ink", + }); + + expect(result.messageId).toBe("msg-uuid-unknown"); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0]?.[0]).toContain("Private API status unknown"); + + const sendCall = mockFetch.mock.calls[1]; + const body = JSON.parse(sendCall[1].body); + expect(body.method).toBeUndefined(); + expect(body.selectedMessageGuid).toBeUndefined(); + expect(body.partIndex).toBeUndefined(); + expect(body.effectId).toBeUndefined(); + } finally { + warnSpy.mockRestore(); + } + }); + it("sends message with chat_guid target directly", async () => { mockFetch.mockResolvedValueOnce({ ok: true, diff --git a/extensions/bluebubbles/src/send.ts b/extensions/bluebubbles/src/send.ts index 62644ca9d53..1530d1702c2 100644 --- a/extensions/bluebubbles/src/send.ts +++ b/extensions/bluebubbles/src/send.ts @@ -379,6 +379,17 @@ export async function sendMessageBlueBubbles( "BlueBubbles send failed: reply/effect requires Private API, but it is disabled on the BlueBubbles server.", ); } + if (needsPrivateApi && privateApiStatus === null) { + const requested = [ + wantsReplyThread ? "reply threading" : null, + wantsEffect ? "message effects" : null, + ] + .filter(Boolean) + .join(" + "); + console.warn( + `[bluebubbles] Private API status unknown; sending without ${requested}. Run a status probe to restore private-api features.`, + ); + } const payload: Record = { chatGuid, tempGuid: crypto.randomUUID(),