import { buildDmGroupAccountAllowlistAdapter } from "openclaw/plugin-sdk/allowlist-config-edit"; import { createChatChannelPlugin } from "openclaw/plugin-sdk/core"; import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-runtime"; import { buildOutboundBaseSessionKey, type RoutePeer } from "openclaw/plugin-sdk/routing"; import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers"; import { chunkTextForOutbound, collectStatusIssuesFromLastError, DEFAULT_ACCOUNT_ID, formatTrimmedAllowFromEntries, normalizeIMessageMessagingTarget, type ChannelPlugin, } from "../runtime-api.js"; import { resolveIMessageAccount, type ResolvedIMessageAccount } from "./accounts.js"; import { createIMessageConversationBindingManager } from "./conversation-bindings.js"; import { matchIMessageAcpConversation, normalizeIMessageAcpConversationId, resolveIMessageConversationIdFromTarget, } from "./conversation-id.js"; import { resolveIMessageGroupRequireMention, resolveIMessageGroupToolPolicy, } from "./group-policy.js"; import type { IMessageProbe } from "./probe.js"; import { imessageSetupAdapter } from "./setup-core.js"; import { createIMessagePluginBase, imessageConfigAdapter, imessageSecurityAdapter, imessageSetupWizard, } from "./shared.js"; import { inferIMessageTargetChatType, looksLikeIMessageExplicitTargetId, normalizeIMessageHandle, parseIMessageTarget, } from "./targets.js"; const loadIMessageChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js")); function buildIMessageBaseSessionKey(params: { cfg: Parameters[0]["cfg"]; agentId: string; accountId?: string | null; peer: RoutePeer; }) { return buildOutboundBaseSessionKey({ ...params, channel: "imessage" }); } function resolveIMessageOutboundSessionRoute(params: { cfg: Parameters[0]["cfg"]; agentId: string; accountId?: string | null; target: string; }) { const parsed = parseIMessageTarget(params.target); if (parsed.kind === "handle") { const handle = normalizeIMessageHandle(parsed.to); if (!handle) { return null; } const peer: RoutePeer = { kind: "direct", id: handle }; const baseSessionKey = buildIMessageBaseSessionKey({ cfg: params.cfg, agentId: params.agentId, accountId: params.accountId, peer, }); return { sessionKey: baseSessionKey, baseSessionKey, peer, chatType: "direct" as const, from: `imessage:${handle}`, to: `imessage:${handle}`, }; } const peerId = parsed.kind === "chat_id" ? String(parsed.chatId) : parsed.kind === "chat_guid" ? parsed.chatGuid : parsed.chatIdentifier; if (!peerId) { return null; } const peer: RoutePeer = { kind: "group", id: peerId }; const baseSessionKey = buildIMessageBaseSessionKey({ cfg: params.cfg, agentId: params.agentId, accountId: params.accountId, peer, }); const toPrefix = parsed.kind === "chat_id" ? "chat_id" : parsed.kind === "chat_guid" ? "chat_guid" : "chat_identifier"; return { sessionKey: baseSessionKey, baseSessionKey, peer, chatType: "group" as const, from: `imessage:group:${peerId}`, to: `${toPrefix}:${peerId}`, }; } export const imessagePlugin: ChannelPlugin = createChatChannelPlugin({ base: { ...createIMessagePluginBase({ setupWizard: imessageSetupWizard, setup: imessageSetupAdapter, }), allowlist: buildDmGroupAccountAllowlistAdapter({ channelId: "imessage", resolveAccount: resolveIMessageAccount, normalize: ({ values }) => formatTrimmedAllowFromEntries(values), resolveDmAllowFrom: (account) => account.config.allowFrom, resolveGroupAllowFrom: (account) => account.config.groupAllowFrom, resolveDmPolicy: (account) => account.config.dmPolicy, resolveGroupPolicy: (account) => account.config.groupPolicy, }), groups: { resolveRequireMention: resolveIMessageGroupRequireMention, resolveToolPolicy: resolveIMessageGroupToolPolicy, }, conversationBindings: { supportsCurrentConversationBinding: true, createManager: ({ cfg, accountId }) => createIMessageConversationBindingManager({ cfg, accountId: accountId ?? undefined, }), }, bindings: { compileConfiguredBinding: ({ conversationId }) => normalizeIMessageAcpConversationId(conversationId), matchInboundConversation: ({ compiledBinding, conversationId }) => matchIMessageAcpConversation({ bindingConversationId: compiledBinding.conversationId, conversationId, }), resolveCommandConversation: ({ originatingTo, commandTo, fallbackTo }) => { const conversationId = resolveIMessageConversationIdFromTarget(originatingTo ?? "") ?? resolveIMessageConversationIdFromTarget(commandTo ?? "") ?? resolveIMessageConversationIdFromTarget(fallbackTo ?? ""); return conversationId ? { conversationId } : null; }, }, messaging: { normalizeTarget: normalizeIMessageMessagingTarget, inferTargetChatType: ({ to }) => inferIMessageTargetChatType(to), resolveOutboundSessionRoute: (params) => resolveIMessageOutboundSessionRoute(params), targetResolver: { looksLikeId: looksLikeIMessageExplicitTargetId, hint: "", resolveTarget: async ({ normalized }) => { const to = normalized?.trim(); if (!to) { return null; } const chatType = inferIMessageTargetChatType(to); if (!chatType) { return null; } return { to, kind: chatType === "direct" ? "user" : "group", source: "normalized" as const, }; }, }, }, status: createComputedAccountStatusAdapter({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { cliPath: null, dbPath: null, }), collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("imessage", accounts), buildChannelSummary: ({ snapshot }) => buildPassiveProbedChannelStatusSummary(snapshot, { cliPath: snapshot.cliPath ?? null, dbPath: snapshot.dbPath ?? null, }), probeAccount: async ({ timeoutMs }) => await (await loadIMessageChannelRuntime()).probeIMessageAccount(timeoutMs), resolveAccountSnapshot: ({ account, runtime }) => ({ accountId: account.accountId, name: account.name, enabled: account.enabled, configured: account.configured, extra: { cliPath: runtime?.cliPath ?? account.config.cliPath ?? null, dbPath: runtime?.dbPath ?? account.config.dbPath ?? null, }, }), resolveAccountState: ({ enabled }) => (enabled ? "enabled" : "disabled"), }), gateway: { startAccount: async (ctx) => { const conversationBindings = createIMessageConversationBindingManager({ cfg: ctx.cfg, accountId: ctx.accountId, }); try { return await (await loadIMessageChannelRuntime()).startIMessageGatewayAccount(ctx); } finally { conversationBindings.stop(); } }, }, }, pairing: { text: { idLabel: "imessageSenderId", message: "OpenClaw: your access has been approved.", notify: async ({ id }) => await (await loadIMessageChannelRuntime()).notifyIMessageApproval(id), }, }, security: imessageSecurityAdapter, outbound: { base: { deliveryMode: "direct", chunker: chunkTextForOutbound, chunkerMode: "text", textChunkLimit: 4000, }, attachedResults: { channel: "imessage", sendText: async ({ cfg, to, text, accountId, deps, replyToId }) => await ( await loadIMessageChannelRuntime() ).sendIMessageOutbound({ cfg, to, text, accountId: accountId ?? undefined, deps, replyToId: replyToId ?? undefined, }), sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, deps, replyToId, }) => await ( await loadIMessageChannelRuntime() ).sendIMessageOutbound({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId: accountId ?? undefined, deps, replyToId: replyToId ?? undefined, }), }, }, });