| import type { BaseProbeResult } from "openclaw/plugin-sdk/bluebubbles"; |
| import { normalizeSecretInputString } from "./secret-input.js"; |
| import { buildBlueBubblesApiUrl, blueBubblesFetchWithTimeout } from "./types.js"; |
|
|
| export type BlueBubblesProbe = BaseProbeResult & { |
| status?: number | null; |
| }; |
|
|
| export type BlueBubblesServerInfo = { |
| os_version?: string; |
| server_version?: string; |
| private_api?: boolean; |
| helper_connected?: boolean; |
| proxy_service?: string; |
| detected_icloud?: string; |
| computer_id?: string; |
| }; |
|
|
| |
| |
| const MAX_SERVER_INFO_CACHE_SIZE = 64; |
| const serverInfoCache = new Map<string, { info: BlueBubblesServerInfo; expires: number }>(); |
| const CACHE_TTL_MS = 10 * 60 * 1000; |
|
|
| function buildCacheKey(accountId?: string): string { |
| return accountId?.trim() || "default"; |
| } |
|
|
| |
| |
| |
| |
| export async function fetchBlueBubblesServerInfo(params: { |
| baseUrl?: string | null; |
| password?: string | null; |
| accountId?: string; |
| timeoutMs?: number; |
| }): Promise<BlueBubblesServerInfo | null> { |
| const baseUrl = normalizeSecretInputString(params.baseUrl); |
| const password = normalizeSecretInputString(params.password); |
| if (!baseUrl || !password) { |
| return null; |
| } |
|
|
| const cacheKey = buildCacheKey(params.accountId); |
| const cached = serverInfoCache.get(cacheKey); |
| if (cached && cached.expires > Date.now()) { |
| return cached.info; |
| } |
|
|
| const url = buildBlueBubblesApiUrl({ baseUrl, path: "/api/v1/server/info", password }); |
| try { |
| const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, params.timeoutMs ?? 5000); |
| if (!res.ok) { |
| return null; |
| } |
| const payload = (await res.json().catch(() => null)) as Record<string, unknown> | null; |
| const data = payload?.data as BlueBubblesServerInfo | undefined; |
| if (data) { |
| serverInfoCache.set(cacheKey, { info: data, expires: Date.now() + CACHE_TTL_MS }); |
| |
| if (serverInfoCache.size > MAX_SERVER_INFO_CACHE_SIZE) { |
| const oldest = serverInfoCache.keys().next().value; |
| if (oldest !== undefined) { |
| serverInfoCache.delete(oldest); |
| } |
| } |
| } |
| return data ?? null; |
| } catch { |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function getCachedBlueBubblesServerInfo(accountId?: string): BlueBubblesServerInfo | null { |
| const cacheKey = buildCacheKey(accountId); |
| const cached = serverInfoCache.get(cacheKey); |
| if (cached && cached.expires > Date.now()) { |
| return cached.info; |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| |
| export function getCachedBlueBubblesPrivateApiStatus(accountId?: string): boolean | null { |
| const info = getCachedBlueBubblesServerInfo(accountId); |
| if (!info || typeof info.private_api !== "boolean") { |
| return null; |
| } |
| return info.private_api; |
| } |
|
|
| export function isBlueBubblesPrivateApiStatusEnabled(status: boolean | null): boolean { |
| return status === true; |
| } |
|
|
| export function isBlueBubblesPrivateApiEnabled(accountId?: string): boolean { |
| return isBlueBubblesPrivateApiStatusEnabled(getCachedBlueBubblesPrivateApiStatus(accountId)); |
| } |
|
|
| |
| |
| |
| export function parseMacOSMajorVersion(version?: string | null): number | null { |
| if (!version) { |
| return null; |
| } |
| const match = /^(\d+)/.exec(version.trim()); |
| return match ? Number.parseInt(match[1], 10) : null; |
| } |
|
|
| |
| |
| |
| |
| export function isMacOS26OrHigher(accountId?: string): boolean { |
| const info = getCachedBlueBubblesServerInfo(accountId); |
| if (!info?.os_version) { |
| return false; |
| } |
| const major = parseMacOSMajorVersion(info.os_version); |
| return major !== null && major >= 26; |
| } |
|
|
| |
| export function clearServerInfoCache(): void { |
| serverInfoCache.clear(); |
| } |
|
|
| export async function probeBlueBubbles(params: { |
| baseUrl?: string | null; |
| password?: string | null; |
| timeoutMs?: number; |
| }): Promise<BlueBubblesProbe> { |
| const baseUrl = normalizeSecretInputString(params.baseUrl); |
| const password = normalizeSecretInputString(params.password); |
| if (!baseUrl) { |
| return { ok: false, error: "serverUrl not configured" }; |
| } |
| if (!password) { |
| return { ok: false, error: "password not configured" }; |
| } |
| const url = buildBlueBubblesApiUrl({ baseUrl, path: "/api/v1/ping", password }); |
| try { |
| const res = await blueBubblesFetchWithTimeout(url, { method: "GET" }, params.timeoutMs); |
| if (!res.ok) { |
| return { ok: false, status: res.status, error: `HTTP ${res.status}` }; |
| } |
| return { ok: true, status: res.status }; |
| } catch (err) { |
| return { |
| ok: false, |
| status: null, |
| error: err instanceof Error ? err.message : String(err), |
| }; |
| } |
| } |
|
|