bluebubbles: gracefully handle disabled private API with action/tool filtering and fallbacks (#16002)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 243cc0cc9a
Co-authored-by: tyler6204 <243?+tyler6204@users.noreply.github.com>
Co-authored-by: tyler6204 <64381258+tyler6204@users.noreply.github.com>
Reviewed-by: @tyler6204
This commit is contained in:
Tyler Yust
2026-02-13 21:15:56 -08:00
committed by GitHub
parent d8beddc8b7
commit 45e12d2388
13 changed files with 305 additions and 23 deletions

View File

@@ -2,6 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
import crypto from "node:crypto";
import path from "node:path";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
export type BlueBubblesChatOpts = {
@@ -25,7 +26,15 @@ function resolveAccount(params: BlueBubblesChatOpts) {
if (!password) {
throw new Error("BlueBubbles password is required");
}
return { baseUrl, password };
return { baseUrl, password, accountId: account.accountId };
}
function assertPrivateApiEnabled(accountId: string, feature: string): void {
if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
throw new Error(
`BlueBubbles ${feature} requires Private API, but it is disabled on the BlueBubbles server.`,
);
}
}
export async function markBlueBubblesChatRead(
@@ -36,7 +45,10 @@ export async function markBlueBubblesChatRead(
if (!trimmed) {
return;
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
return;
}
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmed)}/read`,
@@ -58,7 +70,10 @@ export async function sendBlueBubblesTyping(
if (!trimmed) {
return;
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
return;
}
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmed)}/typing`,
@@ -93,7 +108,8 @@ export async function editBlueBubblesMessage(
throw new Error("BlueBubbles edit requires newText");
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "edit");
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/message/${encodeURIComponent(trimmedGuid)}/edit`,
@@ -135,7 +151,8 @@ export async function unsendBlueBubblesMessage(
throw new Error("BlueBubbles unsend requires messageGuid");
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "unsend");
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/message/${encodeURIComponent(trimmedGuid)}/unsend`,
@@ -175,7 +192,8 @@ export async function renameBlueBubblesChat(
throw new Error("BlueBubbles rename requires chatGuid");
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "renameGroup");
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}`,
@@ -215,7 +233,8 @@ export async function addBlueBubblesParticipant(
throw new Error("BlueBubbles addParticipant requires address");
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "addParticipant");
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/participant`,
@@ -255,7 +274,8 @@ export async function removeBlueBubblesParticipant(
throw new Error("BlueBubbles removeParticipant requires address");
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "removeParticipant");
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/participant`,
@@ -292,7 +312,8 @@ export async function leaveBlueBubblesChat(
throw new Error("BlueBubbles leaveChat requires chatGuid");
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "leaveGroup");
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/leave`,
@@ -325,7 +346,8 @@ export async function setGroupIconBlueBubbles(
throw new Error("BlueBubbles setGroupIcon requires image buffer");
}
const { baseUrl, password } = resolveAccount(opts);
const { baseUrl, password, accountId } = resolveAccount(opts);
assertPrivateApiEnabled(accountId, "setGroupIcon");
const url = buildBlueBubblesApiUrl({
baseUrl,
path: `/api/v1/chat/${encodeURIComponent(trimmedGuid)}/icon`,