UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

423 lines (388 loc) 12.7 kB
export type BlueBubblesService = "imessage" | "sms" | "auto"; export type BlueBubblesTarget = | { kind: "chat_id"; chatId: number } | { kind: "chat_guid"; chatGuid: string } | { kind: "chat_identifier"; chatIdentifier: string } | { kind: "handle"; to: string; service: BlueBubblesService }; export type BlueBubblesAllowTarget = | { kind: "chat_id"; chatId: number } | { kind: "chat_guid"; chatGuid: string } | { kind: "chat_identifier"; chatIdentifier: string } | { kind: "handle"; handle: string }; 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); } export 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, ""); } /** * Extracts the handle from a chat_guid if it's a DM (1:1 chat). * BlueBubbles chat_guid format for DM: "service;-;handle" (e.g., "iMessage;-;+19257864429") * Group chat format: "service;+;groupId" (has "+" instead of "-") */ export function extractHandleFromChatGuid(chatGuid: string): string | null { const parts = chatGuid.split(";"); // DM format: service;-;handle (3 parts, middle is "-") if (parts.length === 3 && parts[1] === "-") { const handle = parts[2]?.trim(); if (handle) { return normalizeBlueBubblesHandle(handle); } } return null; } export function normalizeBlueBubblesMessagingTarget(raw: string): string | undefined { let trimmed = raw.trim(); if (!trimmed) { return undefined; } trimmed = stripBlueBubblesPrefix(trimmed); if (!trimmed) { return undefined; } try { const parsed = parseBlueBubblesTarget(trimmed); if (parsed.kind === "chat_id") { return `chat_id:${parsed.chatId}`; } if (parsed.kind === "chat_guid") { // For DM chat_guids, normalize to just the handle for easier comparison. // This allows "chat_guid:iMessage;-;+1234567890" to match "+1234567890". const handle = extractHandleFromChatGuid(parsed.chatGuid); if (handle) { return handle; } // For group chats or unrecognized formats, keep the full chat_guid return `chat_guid:${parsed.chatGuid}`; } if (parsed.kind === "chat_identifier") { return `chat_identifier:${parsed.chatIdentifier}`; } const handle = normalizeBlueBubblesHandle(parsed.to); if (!handle) { return undefined; } return parsed.service === "auto" ? handle : `${parsed.service}:${handle}`; } catch { return trimmed; } } export function looksLikeBlueBubblesTargetId(raw: string, normalized?: string): boolean { const trimmed = raw.trim(); if (!trimmed) { return false; } const candidate = stripBlueBubblesPrefix(trimmed); if (!candidate) { return false; } if (parseRawChatGuid(candidate)) { return true; } const lowered = candidate.toLowerCase(); if (/^(imessage|sms|auto):/.test(lowered)) { return true; } if ( /^(chat_id|chatid|chat|chat_guid|chatguid|guid|chat_identifier|chatidentifier|chatident|group):/.test( lowered, ) ) { return true; } // Recognize chat<digits> patterns (e.g., "chat660250192681427962") as chat IDs if (/^chat\d+$/i.test(candidate)) { return true; } if (looksLikeRawChatIdentifier(candidate)) { return true; } if (candidate.includes("@")) { return true; } const digitsOnly = candidate.replace(/[\s().-]/g, ""); if (/^\+?\d{3,}$/.test(digitsOnly)) { return true; } if (normalized) { const normalizedTrimmed = normalized.trim(); if (!normalizedTrimmed) { return false; } const normalizedLower = normalizedTrimmed.toLowerCase(); if ( /^(imessage|sms|auto):/.test(normalizedLower) || /^(chat_id|chat_guid|chat_identifier):/.test(normalizedLower) ) { return true; } } return false; } export function parseBlueBubblesTarget(raw: string): BlueBubblesTarget { const trimmed = stripBlueBubblesPrefix(raw); if (!trimmed) { throw new Error("BlueBubbles target is required"); } const lower = trimmed.toLowerCase(); for (const { prefix, service } of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = stripPrefix(trimmed, prefix); if (!remainder) { throw new Error(`${prefix} target is required`); } const remainderLower = remainder.toLowerCase(); const isChatTarget = 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:"); if (isChatTarget) { return parseBlueBubblesTarget(remainder); } return { kind: "handle", to: remainder, service }; } } for (const prefix of CHAT_ID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); const chatId = Number.parseInt(value, 10); if (!Number.isFinite(chatId)) { throw new Error(`Invalid chat_id: ${value}`); } return { kind: "chat_id", chatId }; } } for (const prefix of CHAT_GUID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); if (!value) { throw new Error("chat_guid is required"); } return { kind: "chat_guid", chatGuid: value }; } } for (const prefix of CHAT_IDENTIFIER_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); if (!value) { throw new Error("chat_identifier is required"); } return { kind: "chat_identifier", chatIdentifier: value }; } } if (lower.startsWith("group:")) { const value = stripPrefix(trimmed, "group:"); const chatId = Number.parseInt(value, 10); if (Number.isFinite(chatId)) { return { kind: "chat_id", chatId }; } if (!value) { throw new Error("group target is required"); } return { kind: "chat_guid", chatGuid: value }; } const rawChatGuid = parseRawChatGuid(trimmed); if (rawChatGuid) { return { kind: "chat_guid", chatGuid: rawChatGuid }; } // Handle chat<digits> pattern (e.g., "chat660250192681427962") as chat_identifier // These are BlueBubbles chat identifiers (the third part of a chat GUID), not numeric IDs if (/^chat\d+$/i.test(trimmed)) { return { kind: "chat_identifier", chatIdentifier: trimmed }; } // Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc") if (looksLikeRawChatIdentifier(trimmed)) { return { kind: "chat_identifier", chatIdentifier: trimmed }; } return { kind: "handle", to: trimmed, service: "auto" }; } export function parseBlueBubblesAllowTarget(raw: string): BlueBubblesAllowTarget { const trimmed = raw.trim(); if (!trimmed) { return { kind: "handle", handle: "" }; } const lower = trimmed.toLowerCase(); for (const { prefix } of SERVICE_PREFIXES) { if (lower.startsWith(prefix)) { const remainder = stripPrefix(trimmed, prefix); if (!remainder) { return { kind: "handle", handle: "" }; } return parseBlueBubblesAllowTarget(remainder); } } for (const prefix of CHAT_ID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); const chatId = Number.parseInt(value, 10); if (Number.isFinite(chatId)) { return { kind: "chat_id", chatId }; } } } for (const prefix of CHAT_GUID_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); if (value) { return { kind: "chat_guid", chatGuid: value }; } } } for (const prefix of CHAT_IDENTIFIER_PREFIXES) { if (lower.startsWith(prefix)) { const value = stripPrefix(trimmed, prefix); if (value) { return { kind: "chat_identifier", chatIdentifier: value }; } } } if (lower.startsWith("group:")) { const value = stripPrefix(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 }; } } // Handle chat<digits> pattern (e.g., "chat660250192681427962") as chat_identifier // These are BlueBubbles chat identifiers (the third part of a chat GUID), not numeric IDs if (/^chat\d+$/i.test(trimmed)) { return { kind: "chat_identifier", chatIdentifier: trimmed }; } // Handle UUID/hex chat identifiers (e.g., "8b9c1a10536d4d86a336ea03ab7151cc") if (looksLikeRawChatIdentifier(trimmed)) { return { kind: "chat_identifier", chatIdentifier: trimmed }; } return { kind: "handle", handle: normalizeBlueBubblesHandle(trimmed) }; } export function isAllowedBlueBubblesSender(params: { allowFrom: Array<string | number>; sender: string; chatId?: number | null; chatGuid?: string | null; chatIdentifier?: string | null; }): boolean { const allowFrom = params.allowFrom.map((entry) => String(entry).trim()); if (allowFrom.length === 0) { return true; } if (allowFrom.includes("*")) { return true; } const senderNormalized = normalizeBlueBubblesHandle(params.sender); const chatId = params.chatId ?? undefined; const chatGuid = params.chatGuid?.trim(); const chatIdentifier = params.chatIdentifier?.trim(); for (const entry of allowFrom) { if (!entry) { continue; } const parsed = parseBlueBubblesAllowTarget(entry); if (parsed.kind === "chat_id" && chatId !== undefined) { if (parsed.chatId === chatId) { return true; } } else if (parsed.kind === "chat_guid" && chatGuid) { if (parsed.chatGuid === chatGuid) { return true; } } else if (parsed.kind === "chat_identifier" && chatIdentifier) { if (parsed.chatIdentifier === chatIdentifier) { return true; } } else if (parsed.kind === "handle" && senderNormalized) { if (parsed.handle === senderNormalized) { return true; } } } return false; } export function formatBlueBubblesChatTarget(params: { chatId?: number | null; chatGuid?: string | null; chatIdentifier?: string | null; }): string { if (params.chatId && Number.isFinite(params.chatId)) { return `chat_id:${params.chatId}`; } const guid = params.chatGuid?.trim(); if (guid) { return `chat_guid:${guid}`; } const identifier = params.chatIdentifier?.trim(); if (identifier) { return `chat_identifier:${identifier}`; } return ""; }