import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { createBlueBubblesClient } from "./client.js"; import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js"; import type { OpenClawConfig } from "./runtime-api.js";
/** * Strict normalizer: throws when the input does not map to a supported * BlueBubbles reaction type. Use this for validator-style callers that * need to detect unsupported input (e.g. config sanity checks) rather * than gracefully substituting a fallback.
*/
export function normalizeBlueBubblesReactionInputStrict(emoji: string, remove?: boolean): string { const trimmed = emoji.trim(); if (!trimmed) { thrownew Error("BlueBubbles reaction requires an emoji or name.");
}
let raw = normalizeLowercaseStringOrEmpty(trimmed); if (raw.startsWith("-")) {
raw = raw.slice(1);
} const aliased = REACTION_ALIASES.get(raw) ?? raw; const mapped = REACTION_EMOJIS.get(trimmed) ?? REACTION_EMOJIS.get(raw) ?? aliased; if (!REACTION_TYPES.has(mapped)) { const error = new Error(`Unsupported BlueBubbles reaction: ${trimmed}`);
error.name = UNSUPPORTED_REACTION_ERROR; throw error;
} return remove ? `-${mapped}` : mapped;
}
/** * Lenient normalizer: when the input does not map to a supported * BlueBubbles reaction type (iMessage tapback only supports * love/like/dislike/laugh/emphasize/question), fall back to `love` * so agents that react with a wider emoji vocabulary (e.g. to * ack "seen, working on it") still produce a visible tapback instead * of failing the whole reaction request. * * Contract errors (empty input) continue to bubble up so callers * still catch misuse. * * Use this for model-facing paths. Callers that need to detect * unsupported input should use {@link normalizeBlueBubblesReactionInputStrict}.
*/
export function normalizeBlueBubblesReactionInput(emoji: string, remove?: boolean): string { try { return normalizeBlueBubblesReactionInputStrict(emoji, remove);
} catch (error) { if (error instanceof Error && error.name === UNSUPPORTED_REACTION_ERROR) { return remove ? "-love" : "love";
} throw error;
}
}
export async function sendBlueBubblesReaction(params: {
chatGuid: string;
messageGuid: string;
emoji: string;
remove?: boolean;
partIndex?: number;
opts?: BlueBubblesReactionOpts;
}): Promise<void> { const chatGuid = params.chatGuid.trim(); const messageGuid = params.messageGuid.trim(); if (!chatGuid) { thrownew Error("BlueBubbles reaction requires chatGuid.");
} if (!messageGuid) { thrownew Error("BlueBubbles reaction requires messageGuid.");
} const reaction = normalizeBlueBubblesReactionInput(params.emoji, params.remove); const client = createBlueBubblesClient(params.opts ?? {}); if (getCachedBlueBubblesPrivateApiStatus(client.accountId) === false) { thrownew Error( "BlueBubbles reaction requires Private API, but it is disabled on the BlueBubbles server.",
);
} // Go through the client's typed `react` method — it uses the same SSRF policy // as every other client call, eliminating the asymmetric `{}` vs // `{ allowedHostnames }` path that caused #59722. const res = await client.react({
chatGuid,
selectedMessageGuid: messageGuid,
reaction,
partIndex: typeof params.partIndex === "number" ? params.partIndex : 0,
timeoutMs: params.opts?.timeoutMs,
}); if (!res.ok) { const errorText = await res.text(); thrownew Error(`BlueBubbles reaction failed (${res.status}): ${errorText || "unknown"}`);
}
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-07)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.