UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

583 lines (582 loc) 22 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js"; import { C as resolveExpiresAtMsFromDurationMs, m as isFutureDateTimestampMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js"; import { l as normalizeE164 } from "./utils-CCC-BEJH.js"; import "./number-runtime-DBLVDypr.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { t as resolveApprovalApprovers } from "./approval-approvers-CM-4zRvR.js"; import { t as createResolvedApproverActionAuthAdapter } from "./approval-auth-helpers-DU3OxkhL.js"; import { c as listApprovalReactionBindings, d as resolveApprovalReactionTarget, r as buildApprovalReactionHint, s as createApprovalReactionTargetStore } from "./approval-reaction-runtime-BQQ1cR5g.js"; import "./account-resolution-DBB0e3KP.js"; import { a as resolveIMessageAccount } from "./accounts-DzoymhDS.js"; import { o as normalizeIMessageHandle$1 } from "./targets-BhNdlUSi.js"; import { d as getOptionalIMessageRuntime } from "./monitor-reply-cache-DUH5oJzE.js"; //#region extensions/imessage/src/approval-auth.ts function normalizeIMessageApproverId(value) { const raw = String(value).trim(); if (!raw) return; const normalized = normalizeIMessageHandle$1(raw); if (!normalized || normalized.startsWith("chat_id:") || normalized.startsWith("chat_guid:") || normalized.startsWith("chat_identifier:")) return; return normalized; } function normalizeIMessageApproverEntry(value) { return String(value).trim() === "*" ? "*" : normalizeIMessageApproverId(value); } function getIMessageApprovalApprovers(params) { return resolveApprovalApprovers({ allowFrom: resolveIMessageAccount({ cfg: params.cfg, accountId: params.accountId }).config.allowFrom, normalizeApprover: normalizeIMessageApproverEntry }); } const imessageResolvedApproverAuth = createResolvedApproverActionAuthAdapter({ channelLabel: "iMessage", resolveApprovers: ({ cfg, accountId }) => getIMessageApprovalApprovers({ cfg, accountId }), normalizeSenderId: (value) => normalizeIMessageApproverId(value) }); const imessageApprovalAuth = { authorizeActorAction({ cfg, accountId, senderId, approvalKind }) { if (getIMessageApprovalApprovers({ cfg, accountId }).includes("*")) return { authorized: true }; return imessageResolvedApproverAuth.authorizeActorAction({ cfg, accountId, senderId, action: "approve", approvalKind }); } }; //#endregion //#region extensions/imessage/src/monitor/reaction-context.ts const TAPBACK_TEXT_PATTERNS = [ { prefix: "loved", action: "added", emoji: "❤️" }, { prefix: "liked", action: "added", emoji: "👍" }, { prefix: "disliked", action: "added", emoji: "👎" }, { prefix: "laughed at", action: "added", emoji: "😂" }, { prefix: "emphasized", action: "added", emoji: "‼️" }, { prefix: "questioned", action: "added", emoji: "❓" }, { prefix: "removed a heart from", action: "removed", emoji: "❤️" }, { prefix: "removed a like from", action: "removed", emoji: "👍" }, { prefix: "removed a dislike from", action: "removed", emoji: "👎" }, { prefix: "removed a laugh from", action: "removed", emoji: "😂" }, { prefix: "removed an emphasis from", action: "removed", emoji: "‼️" }, { prefix: "removed a question from", action: "removed", emoji: "❓" } ]; function normalizeReactionValue(value) { return typeof value === "string" ? value.trim().replace(/^p:\d+\//iu, "") || void 0 : void 0; } function resolveReactionTargetGuidCandidates(...values) { const candidates = []; for (const value of values) { if (typeof value !== "string") continue; const raw = value.trim(); if (!raw) continue; const normalized = raw.replace(/^p:\d+\//iu, ""); for (const candidate of [normalized, raw]) if (candidate && !candidates.includes(candidate)) candidates.push(candidate); } return candidates; } function resolveTapbackTextContext(bodyText) { const lower = bodyText.toLowerCase(); for (const pattern of TAPBACK_TEXT_PATTERNS) { if (!lower.startsWith(pattern.prefix)) continue; const afterPrefix = bodyText.slice(pattern.prefix.length).trim(); if (!/^["“]/u.test(afterPrefix)) continue; return { action: pattern.action, emoji: pattern.emoji, targetText: afterPrefix.replace(/^["“]/u, "").replace(/["”]$/u, "").trim() }; } return null; } function resolveIMessageReactionContext(message, bodyText) { if (message.is_reaction === true || message.is_tapback === true || typeof message.associated_message_type === "number" && Number.isFinite(message.associated_message_type) && message.associated_message_type >= 2e3 && message.associated_message_type < 4e3) { const targetGuids = resolveReactionTargetGuidCandidates(message.reacted_to_guid, message.associated_message_guid); return { action: message.is_reaction_add === false ? "removed" : "added", emoji: normalizeReactionValue(message.reaction_emoji) ?? normalizeReactionValue(message.reaction_type) ?? "reaction", targetGuid: targetGuids[0], targetGuids }; } return resolveTapbackTextContext(bodyText); } //#endregion //#region extensions/imessage/src/approval-reactions.ts const PERSISTENT_NAMESPACE = "imessage.approval-reactions"; const PERSISTENT_MAX_ENTRIES = 1e3; const DEFAULT_REACTION_TARGET_TTL_MS = 1440 * 60 * 1e3; let resolverRuntimePromise; const pendingReactionPollTargets = /* @__PURE__ */ new Map(); function loadApprovalResolver() { resolverRuntimePromise ??= import("./approval-resolver-C7uY1Coy.js"); return resolverRuntimePromise; } function chatIdToKeyValue(chatId) { if (chatId == null || chatId === "") return null; if (typeof chatId === "number") return Number.isFinite(chatId) && chatId > 0 ? String(chatId) : null; return chatId.trim() || null; } function enumerateConversationKeyForms(conversation) { const forms = []; const chatGuid = conversation.chatGuid?.trim(); if (chatGuid) forms.push(`chat_guid:${chatGuid}`); const chatIdentifier = conversation.chatIdentifier?.trim(); if (chatIdentifier) forms.push(`chat_identifier:${chatIdentifier}`); const chatIdValue = chatIdToKeyValue(conversation.chatId); if (chatIdValue) forms.push(`chat_id:${chatIdValue}`); const handle = conversation.handle?.trim(); if (handle) forms.push(`handle:${handle}`); return forms; } function normalizeConversationKey(conversation) { return enumerateConversationKeyForms(conversation)[0]; } function enumerateReactionTargetKeys(params) { const accountId = params.accountId.trim(); const messageId = params.messageId.trim(); if (!accountId || !messageId) return []; return enumerateConversationKeyForms(params.conversation).map((form) => `${accountId}:${form}:${messageId}`); } function prunePendingReactionPollTargets(nowMs = Date.now()) { for (const [key, target] of pendingReactionPollTargets.entries()) if (!isFutureDateTimestampMs(target.expiresAtMs, { nowMs })) pendingReactionPollTargets.delete(key); } function resolvePendingReactionPollExpiry(ttlMs) { const nowMs = asDateTimestampMs(Date.now()); if (nowMs === void 0) return; const expiresAtMs = resolveExpiresAtMsFromDurationMs(ttlMs ?? DEFAULT_REACTION_TARGET_TTL_MS, { nowMs }) ?? resolveExpiresAtMsFromDurationMs(DEFAULT_REACTION_TARGET_TTL_MS, { nowMs }); if (expiresAtMs === void 0) return; return { ttlMs: expiresAtMs - nowMs, expiresAtMs }; } function normalizePollTargetMessageId(messageId) { return messageId.trim().replace(/^p:\d+\//iu, ""); } function mergePollTargetConversation(left, right) { return { chatGuid: left.chatGuid ?? right.chatGuid, chatIdentifier: left.chatIdentifier ?? right.chatIdentifier, chatId: left.chatId ?? right.chatId, handle: left.handle ?? right.handle }; } function listPendingIMessageApprovalReactionPollTargets(params) { const accountId = params.accountId.trim(); if (!accountId) return []; prunePendingReactionPollTargets(); const targetByApprovalAndMessage = /* @__PURE__ */ new Map(); for (const target of pendingReactionPollTargets.values()) { if (target.accountId !== accountId) continue; const key = `${target.approvalId}:${normalizePollTargetMessageId(target.messageId)}`; const existing = targetByApprovalAndMessage.get(key); if (!existing) { targetByApprovalAndMessage.set(key, target); continue; } targetByApprovalAndMessage.set(key, { ...existing, conversation: mergePollTargetConversation(existing.conversation, target.conversation), expiresAtMs: Math.max(existing.expiresAtMs, target.expiresAtMs) }); } return [...targetByApprovalAndMessage.values()]; } function reportPersistentApprovalReactionError(error) { try { getOptionalIMessageRuntime()?.logging.getChildLogger({ plugin: "imessage", feature: "approval-reaction-state" }).warn("iMessage persistent approval reaction state failed", { error: String(error) }); } catch {} } function readPersistedTarget(value) { const target = value; if (!target || typeof target.approvalId !== "string" || !Array.isArray(target.allowedDecisions)) return null; const allowedDecisions = target.allowedDecisions.map((valueValue) => typeof valueValue === "string" ? normalizeApprovalDecision(valueValue) : null).filter((valueLocal) => Boolean(valueLocal)); if (allowedDecisions.length === 0) return null; return { approvalId: target.approvalId, allowedDecisions, ...target.approvalKind === "exec" || target.approvalKind === "plugin" ? { approvalKind: target.approvalKind } : {} }; } const imessageApprovalReactionTargets = createApprovalReactionTargetStore({ namespace: PERSISTENT_NAMESPACE, maxEntries: PERSISTENT_MAX_ENTRIES, defaultTtlMs: DEFAULT_REACTION_TARGET_TTL_MS, openStore: (params) => getOptionalIMessageRuntime()?.state.openKeyedStore(params), logPersistentError: reportPersistentApprovalReactionError, readPersistedTarget }); function listIMessageApprovalReactionBindings(allowedDecisions) { return listApprovalReactionBindings({ allowedDecisions }); } function buildIMessageApprovalReactionHint(allowedDecisions) { return buildApprovalReactionHint({ allowedDecisions }); } function insertIMessageApprovalReactionHintNearHeader(params) { const lines = params.text.split(/\r?\n/); const idLineIndex = lines.findIndex((line) => /^ID:\s*\S+/.test(line.trim())); if (idLineIndex >= 0) { const before = lines.slice(0, idLineIndex + 1).join("\n"); const after = lines.slice(idLineIndex + 1).join("\n").replace(/^\n+/, ""); return after ? `${before}\n\n${params.hint}\n\n${after}` : `${before}\n\n${params.hint}`; } return `${params.hint}\n\n${params.text}`; } function addIMessageApprovalReactionHintToText(params) { if (/(^|\n)React with:\s*(\n|$)/i.test(params.text)) return params.text; const hint = buildIMessageApprovalReactionHint(params.allowedDecisions); return hint ? insertIMessageApprovalReactionHintNearHeader({ text: params.text, hint }) : params.text; } function appendIMessageApprovalReactionHintForOutboundMessage(text) { if (/(^|\n)React with:\s*(\n|$)/i.test(text)) return text; const binding = extractIMessageApprovalPromptBinding(text); if (!binding) return text; return addIMessageApprovalReactionHintToText({ text, allowedDecisions: binding.allowedDecisions }); } function normalizeApprovalDecision(value) { const normalized = value.trim().toLowerCase(); if (normalized === "always") return "allow-always"; if (normalized === "allow-once" || normalized === "allow-always" || normalized === "deny") return normalized; return null; } const APPROVAL_ID_LINE_RE = /^\s*ID:\s*([A-Za-z0-9][A-Za-z0-9._:-]*)\s*$/i; const APPROVE_COMMAND_LINE_RE = /\/approve(?:@[^\s]+)?\s+([A-Za-z0-9][A-Za-z0-9._:-]*)\s+(.+)$/i; function extractIMessageApprovalPromptBinding(text) { const lines = text.split(/\r?\n/); const idHeaderMatch = lines.map((line) => line.match(APPROVAL_ID_LINE_RE)).find((match) => Boolean(match)); if (!idHeaderMatch) return null; const approvalId = idHeaderMatch[1]; const allowedDecisions = []; for (const line of lines) { const match = line.match(APPROVE_COMMAND_LINE_RE); if (!match || match[1] !== approvalId) continue; const decisions = match[2].split(/[\s|,]+/); for (const decisionText of decisions) { const decision = normalizeApprovalDecision(decisionText); if (decision && !allowedDecisions.includes(decision)) allowedDecisions.push(decision); } } return allowedDecisions.length > 0 ? { approvalId, allowedDecisions } : null; } function registerIMessageApprovalReactionTarget(params) { const approvalId = params.approvalId.trim(); const allowedDecisions = listIMessageApprovalReactionBindings(params.allowedDecisions).map((binding) => binding.decision); if (!approvalId || allowedDecisions.length === 0) return null; const target = { approvalId, allowedDecisions }; const expiry = resolvePendingReactionPollExpiry(params.ttlMs); if (!expiry) return null; const keys = enumerateReactionTargetKeys({ accountId: params.accountId, conversation: params.conversation, messageId: params.messageId }); if (keys.length === 0) return null; for (const key of keys) { imessageApprovalReactionTargets.register(key, target, { ttlMs: expiry.ttlMs }); pendingReactionPollTargets.set(key, { accountId: params.accountId, conversation: params.conversation, messageId: params.messageId, approvalId, allowedDecisions, expiresAtMs: expiry.expiresAtMs }); } prunePendingReactionPollTargets(); return target; } function registerIMessageApprovalReactionTargetForOutboundMessage(params) { const binding = extractIMessageApprovalPromptBinding(params.text); if (!binding) return false; return Boolean(registerIMessageApprovalReactionTarget({ accountId: params.accountId, conversation: params.conversation, messageId: params.messageId, approvalId: binding.approvalId, allowedDecisions: binding.allowedDecisions, ttlMs: params.ttlMs })); } function unregisterIMessageApprovalReactionTarget(params) { const keys = enumerateReactionTargetKeys(params); for (const key of keys) { imessageApprovalReactionTargets.delete(key); pendingReactionPollTargets.delete(key); } } function resolveTarget(params) { const target = resolveApprovalReactionTarget(params); return target ? { approvalId: target.approvalId, decision: target.decision } : null; } async function resolveIMessageApprovalReactionTargetWithPersistence(params) { const keys = enumerateReactionTargetKeys(params); for (const key of keys) { const target = resolveTarget({ target: await imessageApprovalReactionTargets.lookup(key), reactionKey: params.reactionKey }); if (target) return target; } return null; } function readApprovalReactionEvent(message, bodyText) { const reaction = resolveIMessageReactionContext(message, bodyText); if (!reaction) return null; const reactionKey = reaction.emoji.trim(); const candidates = (reaction.targetGuids ?? []).map((value) => value.trim()).filter((value) => value.length > 0); const primary = reaction.targetGuid?.trim() || candidates[0] || ""; const messageIdCandidates = candidates.length > 0 ? candidates : primary ? [primary] : []; const actorHandle = normalizeIMessageHandle$1((message.sender ?? "").trim()); if (!reactionKey || !primary || !actorHandle) return null; const conversation = { ...message.chat_guid?.trim() ? { chatGuid: message.chat_guid.trim() } : {}, ...message.chat_identifier?.trim() ? { chatIdentifier: message.chat_identifier.trim() } : {}, ...chatIdToKeyValue(message.chat_id ?? void 0) ? { chatId: message.chat_id } : {}, ...message.is_group ? {} : { handle: actorHandle } }; if (!normalizeConversationKey(conversation)) return null; return { conversation, messageId: primary, messageIdCandidates, actorHandle, reactionKey, action: reaction.action }; } async function handleIMessageApprovalReaction(params) { const event = readApprovalReactionEvent(params.message, params.bodyText); if (!event) return { handled: false, stopPolling: false }; if (event.action === "removed") return { handled: false, stopPolling: false }; let target = null; let matchedMessageId = null; for (const candidate of event.messageIdCandidates) { target = await resolveIMessageApprovalReactionTargetWithPersistence({ accountId: params.accountId, conversation: event.conversation, messageId: candidate, reactionKey: event.reactionKey }); if (target) { matchedMessageId = candidate; break; } } if (!target) return { handled: false, stopPolling: false }; const approvalKind = target.approvalId.startsWith("plugin:") ? "plugin" : "exec"; if (getIMessageApprovalApprovers({ cfg: params.cfg, accountId: params.accountId }).length === 0) { params.logVerboseMessage?.(`imessage: approval reaction denied id=${target.approvalId}; reactions require explicit approvers`); return { handled: true, stopPolling: false }; } if (!imessageApprovalAuth.authorizeActorAction({ cfg: params.cfg, accountId: params.accountId, senderId: event.actorHandle, action: "approve", approvalKind }).authorized) { params.logVerboseMessage?.(`imessage: approval reaction denied id=${target.approvalId} sender=${event.actorHandle}`); return { handled: true, stopPolling: false }; } const { isApprovalNotFoundError, resolveIMessageApproval } = await loadApprovalResolver(); try { await resolveIMessageApproval({ cfg: params.cfg, approvalId: target.approvalId, decision: target.decision, senderId: event.actorHandle, gatewayUrl: params.gatewayUrl }); for (const candidate of event.messageIdCandidates) unregisterIMessageApprovalReactionTarget({ accountId: params.accountId, conversation: event.conversation, messageId: candidate }); params.logVerboseMessage?.(`imessage: approval reaction resolved id=${target.approvalId} sender=${event.actorHandle} decision=${target.decision} via messageId=${matchedMessageId ?? event.messageId}`); return { handled: true, stopPolling: true, stopPollingReason: "resolved" }; } catch (error) { if (isApprovalNotFoundError(error)) { for (const candidate of event.messageIdCandidates) unregisterIMessageApprovalReactionTarget({ accountId: params.accountId, conversation: event.conversation, messageId: candidate }); params.logVerboseMessage?.(`imessage: approval reaction ignored for expired approval id=${target.approvalId} sender=${event.actorHandle}`); return { handled: true, stopPolling: true, stopPollingReason: "not-found" }; } try { getOptionalIMessageRuntime()?.logging.getChildLogger({ plugin: "imessage", feature: "approval-reactions" }).warn("approval reaction failed", { approvalId: target.approvalId, senderId: event.actorHandle, error: String(error) }); } catch {} params.logVerboseMessage?.(`imessage: approval reaction failed id=${target.approvalId} sender=${event.actorHandle}: ${String(error)}`); return { handled: true, stopPolling: true, stopPollingReason: "resolver-error" }; } } async function maybeResolveIMessageApprovalReaction(params) { return (await handleIMessageApprovalReaction(params)).handled; } //#endregion //#region extensions/imessage/src/normalize.ts const SERVICE_PREFIXES = [ "imessage:", "sms:", "auto:" ]; const CHAT_TARGET_PREFIX_RE = /^(chat_id:|chatid:|chat:|chat_guid:|chatguid:|guid:|chat_identifier:|chatidentifier:|chatident:)/i; function looksLikeHandleOrPhoneTarget(params) { const trimmed = params.raw.trim(); if (!trimmed) return false; if (params.prefixPattern.test(trimmed)) return true; if (trimmed.includes("@")) return true; return (params.phonePattern ?? /^\+?\d{3,}$/).test(trimmed); } function normalizeIMessageHandle(raw) { const trimmed = raw.trim(); if (!trimmed) return ""; const lowered = normalizeLowercaseStringOrEmpty(trimmed); if (lowered.startsWith("imessage:")) return normalizeIMessageHandle(trimmed.slice(9)); if (lowered.startsWith("sms:")) return normalizeIMessageHandle(trimmed.slice(4)); if (lowered.startsWith("auto:")) return normalizeIMessageHandle(trimmed.slice(5)); if (CHAT_TARGET_PREFIX_RE.test(trimmed)) { const prefix = trimmed.match(CHAT_TARGET_PREFIX_RE)?.[0]; if (!prefix) return ""; const value = trimmed.slice(prefix.length).trim(); return `${normalizeLowercaseStringOrEmpty(prefix)}${value}`; } if (trimmed.includes("@")) return normalizeLowercaseStringOrEmpty(trimmed); const normalized = normalizeE164(trimmed); if (normalized) return normalized; return trimmed.replace(/\s+/g, ""); } function normalizeIMessageMessagingTarget(raw) { const trimmed = normalizeOptionalString(raw); if (!trimmed) return; const lower = normalizeLowercaseStringOrEmpty(trimmed); for (const prefix of SERVICE_PREFIXES) if (lower.startsWith(prefix)) { const normalizedHandle = normalizeIMessageHandle(trimmed.slice(prefix.length).trim()); if (!normalizedHandle) return; if (CHAT_TARGET_PREFIX_RE.test(normalizedHandle)) return normalizedHandle; return `${prefix}${normalizedHandle}`; } return normalizeIMessageHandle(trimmed) || void 0; } function looksLikeIMessageTargetId(raw) { const trimmed = normalizeOptionalString(raw); if (!trimmed) return false; if (CHAT_TARGET_PREFIX_RE.test(trimmed)) return true; return looksLikeHandleOrPhoneTarget({ raw: trimmed, prefixPattern: /^(imessage:|sms:|auto:)/i }); } //#endregion export { extractIMessageApprovalPromptBinding as a, maybeResolveIMessageApprovalReaction as c, unregisterIMessageApprovalReactionTarget as d, resolveIMessageReactionContext as f, appendIMessageApprovalReactionHintForOutboundMessage as i, registerIMessageApprovalReactionTarget as l, imessageApprovalAuth as m, normalizeIMessageMessagingTarget as n, handleIMessageApprovalReaction as o, getIMessageApprovalApprovers as p, addIMessageApprovalReactionHintToText as r, listPendingIMessageApprovalReactionPollTargets as s, looksLikeIMessageTargetId as t, registerIMessageApprovalReactionTargetForOutboundMessage as u };