fix: align BlueBubbles private-api null fallback + warning (#23459) (thanks @echoVic)

This commit is contained in:
Peter Steinberger
2026-02-22 11:47:17 +01:00
parent 888b6bc948
commit 37f12eb7ee
3 changed files with 42 additions and 0 deletions

View File

@@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai
- Chat/UI: strip inline reply/audio directive tags (`[[reply_to_current]]`, `[[reply_to:<id>]]`, `[[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.

View File

@@ -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,

View File

@@ -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<string, unknown> = {
chatGuid,
tempGuid: crypto.randomUUID(),