refactor(bluebubbles): centralize private-api status handling

This commit is contained in:
Peter Steinberger
2026-02-22 12:08:08 +01:00
parent 6f7e5f92c3
commit 296b3f49ef
8 changed files with 186 additions and 35 deletions

View File

@@ -2,7 +2,11 @@ import crypto from "node:crypto";
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import { stripMarkdown } from "openclaw/plugin-sdk";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
import {
getCachedBlueBubblesPrivateApiStatus,
isBlueBubblesPrivateApiStatusEnabled,
} from "./probe.js";
import { warnBlueBubbles } from "./runtime.js";
import { extractBlueBubblesMessageId, resolveBlueBubblesSendTarget } from "./send-helpers.js";
import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
import {
@@ -71,6 +75,38 @@ function resolveEffectId(raw?: string): string | undefined {
return raw;
}
type PrivateApiDecision = {
canUsePrivateApi: boolean;
throwEffectDisabledError: boolean;
warningMessage?: string;
};
function resolvePrivateApiDecision(params: {
privateApiStatus: boolean | null;
wantsReplyThread: boolean;
wantsEffect: boolean;
}): PrivateApiDecision {
const { privateApiStatus, wantsReplyThread, wantsEffect } = params;
const needsPrivateApi = wantsReplyThread || wantsEffect;
const canUsePrivateApi =
needsPrivateApi && isBlueBubblesPrivateApiStatusEnabled(privateApiStatus);
const throwEffectDisabledError = wantsEffect && privateApiStatus === false;
if (!needsPrivateApi || privateApiStatus !== null) {
return { canUsePrivateApi, throwEffectDisabledError };
}
const requested = [
wantsReplyThread ? "reply threading" : null,
wantsEffect ? "message effects" : null,
]
.filter(Boolean)
.join(" + ");
return {
canUsePrivateApi,
throwEffectDisabledError,
warningMessage: `Private API status unknown; sending without ${requested}. Run a status probe to restore private-api features.`,
};
}
type BlueBubblesChatRecord = Record<string, unknown>;
function extractChatGuid(chat: BlueBubblesChatRecord): string | null {
@@ -372,41 +408,36 @@ export async function sendMessageBlueBubbles(
const effectId = resolveEffectId(opts.effectId);
const wantsReplyThread = Boolean(opts.replyToMessageGuid?.trim());
const wantsEffect = Boolean(effectId);
const needsPrivateApi = wantsReplyThread || wantsEffect;
const canUsePrivateApi = needsPrivateApi && privateApiStatus === true;
if (wantsEffect && privateApiStatus === false) {
const privateApiDecision = resolvePrivateApiDecision({
privateApiStatus,
wantsReplyThread,
wantsEffect,
});
if (privateApiDecision.throwEffectDisabledError) {
throw new Error(
"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.`,
);
if (privateApiDecision.warningMessage) {
warnBlueBubbles(privateApiDecision.warningMessage);
}
const payload: Record<string, unknown> = {
chatGuid,
tempGuid: crypto.randomUUID(),
message: strippedText,
};
if (canUsePrivateApi) {
if (privateApiDecision.canUsePrivateApi) {
payload.method = "private-api";
}
// Add reply threading support
if (wantsReplyThread && canUsePrivateApi) {
if (wantsReplyThread && privateApiDecision.canUsePrivateApi) {
payload.selectedMessageGuid = opts.replyToMessageGuid;
payload.partIndex = typeof opts.replyToPartIndex === "number" ? opts.replyToPartIndex : 0;
}
// Add message effects support
if (effectId && canUsePrivateApi) {
if (effectId && privateApiDecision.canUsePrivateApi) {
payload.effectId = effectId;
}