Files
openclaw/src/plugin-sdk/bluebubbles.ts
2026-03-29 22:42:06 +01:00

364 lines
12 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import {
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./channel-targets.js";
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
// Narrow plugin-sdk surface for the bundled BlueBubbles plugin.
// Keep this list additive and scoped to the conversation-binding seam only.
type BlueBubblesService = "imessage" | "sms" | "auto";
type BlueBubblesTarget =
| ParsedChatTarget
| { kind: "handle"; to: string; service: BlueBubblesService };
export type BlueBubblesConversationBindingManager = {
stop: () => void;
};
type BlueBubblesFacadeModule = {
createBlueBubblesConversationBindingManager: (params: {
accountId?: string;
cfg: OpenClawConfig;
}) => BlueBubblesConversationBindingManager;
};
function loadBlueBubblesFacadeModule(): BlueBubblesFacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<BlueBubblesFacadeModule>({
dirName: "bluebubbles",
artifactBasename: "api.js",
});
}
export function createBlueBubblesConversationBindingManager(params: {
accountId?: string;
cfg: OpenClawConfig;
}): BlueBubblesConversationBindingManager {
return loadBlueBubblesFacadeModule().createBlueBubblesConversationBindingManager(params);
}
const CHAT_ID_PREFIXES = ["chat_id:", "chatid:", "chat:"];
const CHAT_GUID_PREFIXES = ["chat_guid:", "chatguid:", "guid:"];
const CHAT_IDENTIFIER_PREFIXES = ["chat_identifier:", "chatidentifier:", "chatident:"];
const SERVICE_PREFIXES: Array<{ prefix: string; service: BlueBubblesService }> = [
{ prefix: "imessage:", service: "imessage" },
{ prefix: "sms:", service: "sms" },
{ prefix: "auto:", service: "auto" },
];
const CHAT_IDENTIFIER_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const CHAT_IDENTIFIER_HEX_RE = /^[0-9a-f]{24,64}$/i;
function parseRawChatGuid(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) {
return null;
}
const parts = trimmed.split(";");
if (parts.length !== 3) {
return null;
}
const service = parts[0]?.trim();
const separator = parts[1]?.trim();
const identifier = parts[2]?.trim();
if (!service || !identifier) {
return null;
}
if (separator !== "+" && separator !== "-") {
return null;
}
return `${service};${separator};${identifier}`;
}
function stripPrefix(value: string, prefix: string): string {
return value.slice(prefix.length).trim();
}
function stripBlueBubblesPrefix(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
return "";
}
if (!trimmed.toLowerCase().startsWith("bluebubbles:")) {
return trimmed;
}
return trimmed.slice("bluebubbles:".length).trim();
}
function looksLikeRawChatIdentifier(value: string): boolean {
const trimmed = value.trim();
if (!trimmed) {
return false;
}
if (/^chat\d+$/i.test(trimmed)) {
return true;
}
return CHAT_IDENTIFIER_UUID_RE.test(trimmed) || CHAT_IDENTIFIER_HEX_RE.test(trimmed);
}
function parseGroupTarget(params: {
trimmed: string;
lower: string;
}): { kind: "chat_id"; chatId: number } | { kind: "chat_guid"; chatGuid: string } | null {
if (!params.lower.startsWith("group:")) {
return null;
}
const value = stripPrefix(params.trimmed, "group:");
const chatId = Number.parseInt(value, 10);
if (Number.isFinite(chatId)) {
return { kind: "chat_id", chatId };
}
if (value) {
return { kind: "chat_guid", chatGuid: value };
}
throw new Error("group target is required");
}
function parseRawChatIdentifierTarget(
trimmed: string,
): { kind: "chat_identifier"; chatIdentifier: string } | null {
if (/^chat\d+$/i.test(trimmed)) {
return { kind: "chat_identifier", chatIdentifier: trimmed };
}
if (looksLikeRawChatIdentifier(trimmed)) {
return { kind: "chat_identifier", chatIdentifier: trimmed };
}
return null;
}
function normalizeBlueBubblesHandle(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) {
return "";
}
const lowered = trimmed.toLowerCase();
if (lowered.startsWith("imessage:")) {
return normalizeBlueBubblesHandle(trimmed.slice(9));
}
if (lowered.startsWith("sms:")) {
return normalizeBlueBubblesHandle(trimmed.slice(4));
}
if (lowered.startsWith("auto:")) {
return normalizeBlueBubblesHandle(trimmed.slice(5));
}
if (trimmed.includes("@")) {
return trimmed.toLowerCase();
}
return trimmed.replace(/\s+/g, "");
}
function extractHandleFromChatGuid(chatGuid: string): string | null {
const parts = chatGuid.split(";");
if (parts.length === 3 && parts[1] === "-") {
const handle = parts[2]?.trim();
if (handle) {
return normalizeBlueBubblesHandle(handle);
}
}
return null;
}
function parseBlueBubblesTarget(raw: string): BlueBubblesTarget {
const trimmed = stripBlueBubblesPrefix(raw);
if (!trimmed) {
throw new Error("BlueBubbles target is required");
}
const lower = trimmed.toLowerCase();
const servicePrefixed = resolveServicePrefixedTarget({
trimmed,
lower,
servicePrefixes: SERVICE_PREFIXES,
isChatTarget: (remainderLower) =>
CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
remainderLower.startsWith("group:"),
parseTarget: parseBlueBubblesTarget,
});
if (servicePrefixed) {
return servicePrefixed;
}
const chatTarget = parseChatTargetPrefixesOrThrow({
trimmed,
lower,
chatIdPrefixes: CHAT_ID_PREFIXES,
chatGuidPrefixes: CHAT_GUID_PREFIXES,
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES,
});
if (chatTarget) {
return chatTarget;
}
const groupTarget = parseGroupTarget({ trimmed, lower });
if (groupTarget) {
return groupTarget;
}
const rawChatGuid = parseRawChatGuid(trimmed);
if (rawChatGuid) {
return { kind: "chat_guid", chatGuid: rawChatGuid };
}
const rawChatIdentifierTarget = parseRawChatIdentifierTarget(trimmed);
if (rawChatIdentifierTarget) {
return rawChatIdentifierTarget;
}
return { kind: "handle", to: trimmed, service: "auto" };
}
export function normalizeBlueBubblesAcpConversationId(
conversationId: string,
): { conversationId: string } | null {
const trimmed = conversationId.trim();
if (!trimmed) {
return null;
}
try {
const parsed = parseBlueBubblesTarget(trimmed);
if (parsed.kind === "handle") {
const handle = normalizeBlueBubblesHandle(parsed.to);
return handle ? { conversationId: handle } : null;
}
if (parsed.kind === "chat_id") {
return { conversationId: String(parsed.chatId) };
}
if (parsed.kind === "chat_guid") {
const handle = extractHandleFromChatGuid(parsed.chatGuid);
return {
conversationId: handle || parsed.chatGuid,
};
}
return { conversationId: parsed.chatIdentifier };
} catch {
const handle = normalizeBlueBubblesHandle(trimmed);
return handle ? { conversationId: handle } : null;
}
}
export function matchBlueBubblesAcpConversation(params: {
bindingConversationId: string;
conversationId: string;
}): { conversationId: string; matchPriority: number } | null {
const binding = normalizeBlueBubblesAcpConversationId(params.bindingConversationId);
const conversation = normalizeBlueBubblesAcpConversationId(params.conversationId);
if (!binding || !conversation) {
return null;
}
if (binding.conversationId !== conversation.conversationId) {
return null;
}
return {
conversationId: conversation.conversationId,
matchPriority: 2,
};
}
export function resolveBlueBubblesConversationIdFromTarget(target: string): string | undefined {
return normalizeBlueBubblesAcpConversationId(target)?.conversationId;
}
export { resolveAckReaction } from "../agents/identity.js";
export {
createActionGate,
jsonResult,
readNumberParam,
readReactionParams,
readStringParam,
} from "../agents/tools/common.js";
export type { HistoryEntry } from "../auto-reply/reply/history.js";
export {
evictOldHistoryKeys,
recordPendingHistoryEntryIfEnabled,
} from "../auto-reply/reply/history.js";
export { resolveControlCommandGate } from "../channels/command-gating.js";
export { logAckFailure, logInboundDrop, logTypingFailure } from "../channels/logging.js";
export {
BLUEBUBBLES_ACTION_NAMES,
BLUEBUBBLES_ACTIONS,
} from "../channels/plugins/bluebubbles-actions.js";
export {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "../channels/plugins/config-helpers.js";
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
export {
resolveBlueBubblesGroupRequireMention,
resolveBlueBubblesGroupToolPolicy,
} from "./bluebubbles-policy.js";
export { formatPairingApproveHint } from "../channels/plugins/helpers.js";
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
export {
addWildcardAllowFrom,
mergeAllowFromEntries,
setTopLevelChannelDmPolicyWithAllowFrom,
} from "../channels/plugins/setup-wizard-helpers.js";
export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js";
export {
applyAccountNameToChannelSection,
migrateBaseNameToDefaultAccount,
patchScopedAccountConfig,
} from "../channels/plugins/setup-helpers.js";
export { createAccountListHelpers } from "../channels/plugins/account-helpers.js";
export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js";
export type {
BaseProbeResult,
ChannelAccountSnapshot,
ChannelMessageActionAdapter,
ChannelMessageActionName,
} from "../channels/plugins/types.js";
export type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
export { createChannelReplyPipeline } from "./channel-reply-pipeline.js";
export type { OpenClawConfig } from "../config/config.js";
export type { DmPolicy, GroupPolicy } from "../config/types.js";
export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
export { MarkdownConfigSchema } from "../config/zod-schema.core.js";
export {
parseChatAllowTargetPrefixes,
parseChatTargetPrefixesOrThrow,
resolveServicePrefixedAllowTarget,
resolveServicePrefixedTarget,
type ParsedChatTarget,
} from "./channel-targets.js";
export { stripMarkdown } from "./text-runtime.js";
export { parseFiniteNumber } from "../infra/parse-finite-number.js";
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
export type { PluginRuntime } from "../plugins/runtime/types.js";
export type { OpenClawPluginApi } from "../plugins/types.js";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
export {
DM_GROUP_ACCESS_REASON,
readStoreAllowFromForDmPolicy,
resolveDmGroupAccessWithLists,
} from "../security/dm-policy-shared.js";
export { formatDocsLink } from "../terminal/links.js";
export type { WizardPrompter } from "../wizard/prompts.js";
export { isAllowedParsedChatSender } from "./allow-from.js";
export { readBooleanParam } from "./boolean-param.js";
export { mapAllowFromEntries } from "./channel-config-helpers.js";
export { createChannelPairingController } from "./channel-pairing.js";
export { resolveRequestUrl } from "./request-url.js";
export {
buildComputedAccountStatusSnapshot,
buildProbeChannelStatusSummary,
} from "./status-helpers.js";
export { isAllowedBlueBubblesSender } from "./bluebubbles-policy.js";
export { extractToolSend } from "./tool-send.js";
export {
WEBHOOK_RATE_LIMIT_DEFAULTS,
createFixedWindowRateLimiter,
createWebhookInFlightLimiter,
normalizeWebhookPath,
readWebhookBodyOrReject,
registerWebhookTargetWithPluginRoute,
resolveRequestClientIp,
resolveWebhookTargets,
resolveWebhookTargetWithAuthOrRejectSync,
withResolvedWebhookRequestPipeline,
} from "./webhook-ingress.js";