diff --git a/extensions/whatsapp/src/channel.setup.ts b/extensions/whatsapp/src/channel.setup.ts index df13d0b06f5..919a75c1a8c 100644 --- a/extensions/whatsapp/src/channel.setup.ts +++ b/extensions/whatsapp/src/channel.setup.ts @@ -1,150 +1,21 @@ -import { - buildAccountScopedDmSecurityPolicy, - buildChannelConfigSchema, - collectAllowlistProviderGroupPolicyWarnings, - collectOpenGroupPolicyRouteAllowlistWarnings, - DEFAULT_ACCOUNT_ID, - formatWhatsAppConfigAllowFromEntries, - getChatChannelMeta, - normalizeE164, - resolveWhatsAppConfigAllowFrom, - resolveWhatsAppConfigDefaultTo, - resolveWhatsAppGroupIntroHint, - resolveWhatsAppGroupRequireMention, - resolveWhatsAppGroupToolPolicy, - WhatsAppConfigSchema, - type ChannelPlugin, -} from "../../../src/plugin-sdk-internal/whatsapp.js"; -import { - listWhatsAppAccountIds, - resolveDefaultWhatsAppAccountId, - resolveWhatsAppAccount, - type ResolvedWhatsAppAccount, -} from "./accounts.js"; +import { type ChannelPlugin } from "openclaw/plugin-sdk/whatsapp"; +import { type ResolvedWhatsAppAccount } from "./accounts.js"; import { webAuthExists } from "./auth-store.js"; -import { whatsappSetupWizardProxy } from "./plugin-shared.js"; import { whatsappSetupAdapter } from "./setup-core.js"; +import { createWhatsAppPluginBase, createWhatsAppSetupWizardProxy } from "./shared.js"; + +async function loadWhatsAppChannelRuntime() { + return await import("./channel.runtime.js"); +} + +const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy(async () => ({ + whatsappSetupWizard: (await loadWhatsAppChannelRuntime()).whatsappSetupWizard, +})); export const whatsappSetupPlugin: ChannelPlugin = { - id: "whatsapp", - meta: { - ...getChatChannelMeta("whatsapp"), - showConfigured: false, - quickstartAllowFrom: true, - forceAccountBinding: true, - preferSessionLookupForAnnounceTarget: true, - }, - setupWizard: whatsappSetupWizardProxy, - capabilities: { - chatTypes: ["direct", "group"], - polls: true, - reactions: true, - media: true, - }, - reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] }, - gatewayMethods: ["web.login.start", "web.login.wait"], - configSchema: buildChannelConfigSchema(WhatsAppConfigSchema), - config: { - listAccountIds: (cfg) => listWhatsAppAccountIds(cfg), - resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }), - defaultAccountId: (cfg) => resolveDefaultWhatsAppAccountId(cfg), - setAccountEnabled: ({ cfg, accountId, enabled }) => { - const accountKey = accountId || DEFAULT_ACCOUNT_ID; - const accounts = { ...cfg.channels?.whatsapp?.accounts }; - const existing = accounts[accountKey] ?? {}; - return { - ...cfg, - channels: { - ...cfg.channels, - whatsapp: { - ...cfg.channels?.whatsapp, - accounts: { - ...accounts, - [accountKey]: { - ...existing, - enabled, - }, - }, - }, - }, - }; - }, - deleteAccount: ({ cfg, accountId }) => { - const accountKey = accountId || DEFAULT_ACCOUNT_ID; - const accounts = { ...cfg.channels?.whatsapp?.accounts }; - delete accounts[accountKey]; - return { - ...cfg, - channels: { - ...cfg.channels, - whatsapp: { - ...cfg.channels?.whatsapp, - accounts: Object.keys(accounts).length ? accounts : undefined, - }, - }, - }; - }, - isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false, - disabledReason: () => "disabled", + ...createWhatsAppPluginBase({ + setupWizard: whatsappSetupWizardProxy, + setup: whatsappSetupAdapter, isConfigured: async (account) => await webAuthExists(account.authDir), - unconfiguredReason: () => "not linked", - describeAccount: (account) => ({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: Boolean(account.authDir), - linked: Boolean(account.authDir), - dmPolicy: account.dmPolicy, - allowFrom: account.allowFrom, - }), - resolveAllowFrom: ({ cfg, accountId }) => resolveWhatsAppConfigAllowFrom({ cfg, accountId }), - formatAllowFrom: ({ allowFrom }) => formatWhatsAppConfigAllowFromEntries(allowFrom), - resolveDefaultTo: ({ cfg, accountId }) => resolveWhatsAppConfigDefaultTo({ cfg, accountId }), - }, - security: { - resolveDmPolicy: ({ cfg, accountId, account }) => - buildAccountScopedDmSecurityPolicy({ - cfg, - channelKey: "whatsapp", - accountId, - fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, - policy: account.dmPolicy, - allowFrom: account.allowFrom ?? [], - policyPathSuffix: "dmPolicy", - normalizeEntry: (raw) => normalizeE164(raw), - }), - collectWarnings: ({ account, cfg }) => { - const groupAllowlistConfigured = - Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0; - return collectAllowlistProviderGroupPolicyWarnings({ - cfg, - providerConfigPresent: cfg.channels?.whatsapp !== undefined, - configuredGroupPolicy: account.groupPolicy, - collect: (groupPolicy) => - collectOpenGroupPolicyRouteAllowlistWarnings({ - groupPolicy, - routeAllowlistConfigured: groupAllowlistConfigured, - restrictSenders: { - surface: "WhatsApp groups", - openScope: "any member in allowed groups", - groupPolicyPath: "channels.whatsapp.groupPolicy", - groupAllowFromPath: "channels.whatsapp.groupAllowFrom", - }, - noRouteAllowlist: { - surface: "WhatsApp groups", - routeAllowlistPath: "channels.whatsapp.groups", - routeScope: "group", - groupPolicyPath: "channels.whatsapp.groupPolicy", - groupAllowFromPath: "channels.whatsapp.groupAllowFrom", - }, - }), - }); - }, - }, - setup: whatsappSetupAdapter, - groups: { - resolveRequireMention: resolveWhatsAppGroupRequireMention, - resolveToolPolicy: resolveWhatsAppGroupToolPolicy, - resolveGroupIntroHint: resolveWhatsAppGroupIntroHint, - }, + }), }; diff --git a/extensions/whatsapp/src/channel.ts b/extensions/whatsapp/src/channel.ts index 3f2c2e449dc..6fe1663e55f 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -1,45 +1,34 @@ -import { buildAccountScopedAllowlistConfigEditor } from "../../../src/plugin-sdk-internal/channel-config.js"; +import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/compat"; import { - buildChannelConfigSchema, - buildAccountScopedDmSecurityPolicy, - collectAllowlistProviderGroupPolicyWarnings, - collectOpenGroupPolicyRouteAllowlistWarnings, createActionGate, createWhatsAppOutboundBase, DEFAULT_ACCOUNT_ID, - getChatChannelMeta, listWhatsAppDirectoryGroupsFromConfig, listWhatsAppDirectoryPeersFromConfig, - normalizeE164, formatWhatsAppConfigAllowFromEntries, readStringParam, resolveWhatsAppOutboundTarget, - resolveWhatsAppConfigAllowFrom, - resolveWhatsAppConfigDefaultTo, - resolveWhatsAppGroupRequireMention, - resolveWhatsAppGroupIntroHint, - resolveWhatsAppGroupToolPolicy, resolveWhatsAppHeartbeatRecipients, resolveWhatsAppMentionStripRegexes, - WhatsAppConfigSchema, type ChannelMessageActionName, type ChannelPlugin, -} from "../../../src/plugin-sdk-internal/whatsapp.js"; +} from "openclaw/plugin-sdk/whatsapp"; import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../../src/whatsapp/normalize.js"; // WhatsApp-specific imports from local extension code (moved from src/web/ and src/channels/plugins/) -import { - listWhatsAppAccountIds, - resolveDefaultWhatsAppAccountId, - resolveWhatsAppAccount, - type ResolvedWhatsAppAccount, -} from "./accounts.js"; +import { resolveWhatsAppAccount, type ResolvedWhatsAppAccount } from "./accounts.js"; import { looksLikeWhatsAppTargetId, normalizeWhatsAppMessagingTarget } from "./normalize.js"; -import { whatsappSetupWizardProxy } from "./plugin-shared.js"; import { getWhatsAppRuntime } from "./runtime.js"; import { whatsappSetupAdapter } from "./setup-core.js"; +import { + createWhatsAppPluginBase, + createWhatsAppSetupWizardProxy, + WHATSAPP_CHANNEL, +} from "./shared.js"; import { collectWhatsAppStatusIssues } from "./status-issues.js"; -const meta = getChatChannelMeta("whatsapp"); +async function loadWhatsAppChannelRuntime() { + return await import("./channel.runtime.js"); +} function normalizeWhatsAppPayloadText(text: string | undefined): string { return (text ?? "").replace(/^(?:[ \t]*\r?\n)+/, ""); @@ -56,87 +45,21 @@ function parseWhatsAppExplicitTarget(raw: string) { }; } +const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy(async () => ({ + whatsappSetupWizard: (await loadWhatsAppChannelRuntime()).whatsappSetupWizard, +})); + export const whatsappPlugin: ChannelPlugin = { - id: "whatsapp", - meta: { - ...meta, - showConfigured: false, - quickstartAllowFrom: true, - forceAccountBinding: true, - preferSessionLookupForAnnounceTarget: true, - }, - setupWizard: whatsappSetupWizardProxy, + ...createWhatsAppPluginBase({ + setupWizard: whatsappSetupWizardProxy, + setup: whatsappSetupAdapter, + isConfigured: async (account) => + await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir), + }), agentTools: () => [getWhatsAppRuntime().channel.whatsapp.createLoginTool()], pairing: { idLabel: "whatsappSenderId", }, - capabilities: { - chatTypes: ["direct", "group"], - polls: true, - reactions: true, - media: true, - }, - reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] }, - gatewayMethods: ["web.login.start", "web.login.wait"], - configSchema: buildChannelConfigSchema(WhatsAppConfigSchema), - config: { - listAccountIds: (cfg) => listWhatsAppAccountIds(cfg), - resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }), - defaultAccountId: (cfg) => resolveDefaultWhatsAppAccountId(cfg), - setAccountEnabled: ({ cfg, accountId, enabled }) => { - const accountKey = accountId || DEFAULT_ACCOUNT_ID; - const accounts = { ...cfg.channels?.whatsapp?.accounts }; - const existing = accounts[accountKey] ?? {}; - return { - ...cfg, - channels: { - ...cfg.channels, - whatsapp: { - ...cfg.channels?.whatsapp, - accounts: { - ...accounts, - [accountKey]: { - ...existing, - enabled, - }, - }, - }, - }, - }; - }, - deleteAccount: ({ cfg, accountId }) => { - const accountKey = accountId || DEFAULT_ACCOUNT_ID; - const accounts = { ...cfg.channels?.whatsapp?.accounts }; - delete accounts[accountKey]; - return { - ...cfg, - channels: { - ...cfg.channels, - whatsapp: { - ...cfg.channels?.whatsapp, - accounts: Object.keys(accounts).length ? accounts : undefined, - }, - }, - }; - }, - isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false, - disabledReason: () => "disabled", - isConfigured: async (account) => - await getWhatsAppRuntime().channel.whatsapp.webAuthExists(account.authDir), - unconfiguredReason: () => "not linked", - describeAccount: (account) => ({ - accountId: account.accountId, - name: account.name, - enabled: account.enabled, - configured: Boolean(account.authDir), - linked: Boolean(account.authDir), - dmPolicy: account.dmPolicy, - allowFrom: account.allowFrom, - }), - resolveAllowFrom: ({ cfg, accountId }) => resolveWhatsAppConfigAllowFrom({ cfg, accountId }), - formatAllowFrom: ({ allowFrom }) => formatWhatsAppConfigAllowFromEntries(allowFrom), - resolveDefaultTo: ({ cfg, accountId }) => resolveWhatsAppConfigDefaultTo({ cfg, accountId }), - }, allowlist: { supportsScope: ({ scope }) => scope === "dm" || scope === "group" || scope === "all", readConfig: ({ cfg, accountId }) => { @@ -157,53 +80,6 @@ export const whatsappPlugin: ChannelPlugin = { }), }), }, - security: { - resolveDmPolicy: ({ cfg, accountId, account }) => { - return buildAccountScopedDmSecurityPolicy({ - cfg, - channelKey: "whatsapp", - accountId, - fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, - policy: account.dmPolicy, - allowFrom: account.allowFrom ?? [], - policyPathSuffix: "dmPolicy", - normalizeEntry: (raw) => normalizeE164(raw), - }); - }, - collectWarnings: ({ account, cfg }) => { - const groupAllowlistConfigured = - Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0; - return collectAllowlistProviderGroupPolicyWarnings({ - cfg, - providerConfigPresent: cfg.channels?.whatsapp !== undefined, - configuredGroupPolicy: account.groupPolicy, - collect: (groupPolicy) => - collectOpenGroupPolicyRouteAllowlistWarnings({ - groupPolicy, - routeAllowlistConfigured: groupAllowlistConfigured, - restrictSenders: { - surface: "WhatsApp groups", - openScope: "any member in allowed groups", - groupPolicyPath: "channels.whatsapp.groupPolicy", - groupAllowFromPath: "channels.whatsapp.groupAllowFrom", - }, - noRouteAllowlist: { - surface: "WhatsApp groups", - routeAllowlistPath: "channels.whatsapp.groups", - routeScope: "group", - groupPolicyPath: "channels.whatsapp.groupPolicy", - groupAllowFromPath: "channels.whatsapp.groupAllowFrom", - }, - }), - }); - }, - }, - setup: whatsappSetupAdapter, - groups: { - resolveRequireMention: resolveWhatsAppGroupRequireMention, - resolveToolPolicy: resolveWhatsAppGroupToolPolicy, - resolveGroupIntroHint: resolveWhatsAppGroupIntroHint, - }, mentions: { stripRegexes: ({ ctx }) => resolveWhatsAppMentionStripRegexes(ctx), }, @@ -256,7 +132,7 @@ export const whatsappPlugin: ChannelPlugin = { supportsAction: ({ action }) => action === "react", handleAction: async ({ action, params, cfg, accountId }) => { if (action !== "react") { - throw new Error(`Action ${action} is not supported for provider ${meta.id}.`); + throw new Error(`Action ${action} is not supported for provider ${WHATSAPP_CHANNEL}.`); } const messageId = readStringParam(params, "messageId", { required: true, diff --git a/extensions/whatsapp/src/shared.ts b/extensions/whatsapp/src/shared.ts new file mode 100644 index 00000000000..3a8f7412e7e --- /dev/null +++ b/extensions/whatsapp/src/shared.ts @@ -0,0 +1,212 @@ +import { + buildAccountScopedDmSecurityPolicy, + buildChannelConfigSchema, + collectAllowlistProviderGroupPolicyWarnings, + collectOpenGroupPolicyRouteAllowlistWarnings, + DEFAULT_ACCOUNT_ID, + formatWhatsAppConfigAllowFromEntries, + getChatChannelMeta, + normalizeE164, + resolveWhatsAppConfigAllowFrom, + resolveWhatsAppConfigDefaultTo, + resolveWhatsAppGroupIntroHint, + resolveWhatsAppGroupRequireMention, + resolveWhatsAppGroupToolPolicy, + WhatsAppConfigSchema, + type ChannelPlugin, +} from "openclaw/plugin-sdk/whatsapp"; +import { + listWhatsAppAccountIds, + resolveDefaultWhatsAppAccountId, + resolveWhatsAppAccount, + type ResolvedWhatsAppAccount, +} from "./accounts.js"; + +export const WHATSAPP_CHANNEL = "whatsapp" as const; + +export function createWhatsAppSetupWizardProxy( + loadWizard: () => Promise<{ + whatsappSetupWizard: NonNullable["setupWizard"]>; + }>, +): NonNullable["setupWizard"]> { + return { + channel: WHATSAPP_CHANNEL, + status: { + configuredLabel: "linked", + unconfiguredLabel: "not linked", + configuredHint: "linked", + unconfiguredHint: "not linked", + configuredScore: 5, + unconfiguredScore: 4, + resolveConfigured: async ({ cfg }) => + await (await loadWizard()).whatsappSetupWizard.status.resolveConfigured({ cfg }), + resolveStatusLines: async ({ cfg, configured }) => + (await ( + await loadWizard() + ).whatsappSetupWizard.status.resolveStatusLines?.({ + cfg, + configured, + })) ?? [], + }, + resolveShouldPromptAccountIds: (params) => + (params.shouldPromptAccountIds || params.options?.promptWhatsAppAccountId) ?? false, + credentials: [], + finalize: async (params) => await (await loadWizard()).whatsappSetupWizard.finalize!(params), + disable: (cfg) => ({ + ...cfg, + channels: { + ...cfg.channels, + whatsapp: { + ...cfg.channels?.whatsapp, + enabled: false, + }, + }, + }), + onAccountRecorded: (accountId, options) => { + options?.onWhatsAppAccountId?.(accountId); + }, + }; +} + +export function createWhatsAppPluginBase(params: { + setupWizard: NonNullable["setupWizard"]>; + setup: NonNullable["setup"]>; + isConfigured: NonNullable["config"]>["isConfigured"]; +}): Pick< + ChannelPlugin, + | "id" + | "meta" + | "setupWizard" + | "capabilities" + | "reload" + | "gatewayMethods" + | "configSchema" + | "config" + | "security" + | "setup" + | "groups" +> { + return { + id: WHATSAPP_CHANNEL, + meta: { + ...getChatChannelMeta(WHATSAPP_CHANNEL), + showConfigured: false, + quickstartAllowFrom: true, + forceAccountBinding: true, + preferSessionLookupForAnnounceTarget: true, + }, + setupWizard: params.setupWizard, + capabilities: { + chatTypes: ["direct", "group"], + polls: true, + reactions: true, + media: true, + }, + reload: { configPrefixes: ["web"], noopPrefixes: ["channels.whatsapp"] }, + gatewayMethods: ["web.login.start", "web.login.wait"], + configSchema: buildChannelConfigSchema(WhatsAppConfigSchema), + config: { + listAccountIds: (cfg) => listWhatsAppAccountIds(cfg), + resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }), + defaultAccountId: (cfg) => resolveDefaultWhatsAppAccountId(cfg), + setAccountEnabled: ({ cfg, accountId, enabled }) => { + const accountKey = accountId || DEFAULT_ACCOUNT_ID; + const accounts = { ...cfg.channels?.whatsapp?.accounts }; + const existing = accounts[accountKey] ?? {}; + return { + ...cfg, + channels: { + ...cfg.channels, + whatsapp: { + ...cfg.channels?.whatsapp, + accounts: { + ...accounts, + [accountKey]: { + ...existing, + enabled, + }, + }, + }, + }, + }; + }, + deleteAccount: ({ cfg, accountId }) => { + const accountKey = accountId || DEFAULT_ACCOUNT_ID; + const accounts = { ...cfg.channels?.whatsapp?.accounts }; + delete accounts[accountKey]; + return { + ...cfg, + channels: { + ...cfg.channels, + whatsapp: { + ...cfg.channels?.whatsapp, + accounts: Object.keys(accounts).length ? accounts : undefined, + }, + }, + }; + }, + isEnabled: (account, cfg) => account.enabled && cfg.web?.enabled !== false, + disabledReason: () => "disabled", + isConfigured: params.isConfigured, + unconfiguredReason: () => "not linked", + describeAccount: (account) => ({ + accountId: account.accountId, + name: account.name, + enabled: account.enabled, + configured: Boolean(account.authDir), + linked: Boolean(account.authDir), + dmPolicy: account.dmPolicy, + allowFrom: account.allowFrom, + }), + resolveAllowFrom: ({ cfg, accountId }) => resolveWhatsAppConfigAllowFrom({ cfg, accountId }), + formatAllowFrom: ({ allowFrom }) => formatWhatsAppConfigAllowFromEntries(allowFrom), + resolveDefaultTo: ({ cfg, accountId }) => resolveWhatsAppConfigDefaultTo({ cfg, accountId }), + }, + security: { + resolveDmPolicy: ({ cfg, accountId, account }) => + buildAccountScopedDmSecurityPolicy({ + cfg, + channelKey: WHATSAPP_CHANNEL, + accountId, + fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID, + policy: account.dmPolicy, + allowFrom: account.allowFrom ?? [], + policyPathSuffix: "dmPolicy", + normalizeEntry: (raw) => normalizeE164(raw), + }), + collectWarnings: ({ account, cfg }) => { + const groupAllowlistConfigured = + Boolean(account.groups) && Object.keys(account.groups ?? {}).length > 0; + return collectAllowlistProviderGroupPolicyWarnings({ + cfg, + providerConfigPresent: cfg.channels?.whatsapp !== undefined, + configuredGroupPolicy: account.groupPolicy, + collect: (groupPolicy) => + collectOpenGroupPolicyRouteAllowlistWarnings({ + groupPolicy, + routeAllowlistConfigured: groupAllowlistConfigured, + restrictSenders: { + surface: "WhatsApp groups", + openScope: "any member in allowed groups", + groupPolicyPath: "channels.whatsapp.groupPolicy", + groupAllowFromPath: "channels.whatsapp.groupAllowFrom", + }, + noRouteAllowlist: { + surface: "WhatsApp groups", + routeAllowlistPath: "channels.whatsapp.groups", + routeScope: "group", + groupPolicyPath: "channels.whatsapp.groupPolicy", + groupAllowFromPath: "channels.whatsapp.groupAllowFrom", + }, + }), + }); + }, + }, + setup: params.setup, + groups: { + resolveRequireMention: resolveWhatsAppGroupRequireMention, + resolveToolPolicy: resolveWhatsAppGroupToolPolicy, + resolveGroupIntroHint: resolveWhatsAppGroupIntroHint, + }, + }; +}