diff --git a/extensions/whatsapp/src/channel.setup.ts b/extensions/whatsapp/src/channel.setup.ts index 6cf2a75d1ce..ebe4deb5789 100644 --- a/extensions/whatsapp/src/channel.setup.ts +++ b/extensions/whatsapp/src/channel.setup.ts @@ -1,150 +1,13 @@ -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"; +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, whatsappSetupWizardProxy } from "./shared.js"; 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 dda6215c27f..3bf9bba0c34 100644 --- a/extensions/whatsapp/src/channel.ts +++ b/extensions/whatsapp/src/channel.ts @@ -1,46 +1,30 @@ import { buildAccountScopedAllowlistConfigEditor } from "openclaw/plugin-sdk/allowlist-config-edit"; import { - buildAccountScopedDmSecurityPolicy, - buildChannelConfigSchema, - 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 "openclaw/plugin-sdk/whatsapp"; import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "openclaw/plugin-sdk/whatsapp"; // 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 { loadWhatsAppChannelRuntime, whatsappSetupWizardProxy } from "./plugin-shared.js"; import { getWhatsAppRuntime } from "./runtime.js"; import { whatsappSetupAdapter } from "./setup-core.js"; +import { + createWhatsAppPluginBase, + loadWhatsAppChannelRuntime, + whatsappSetupWizardProxy, + WHATSAPP_CHANNEL, +} from "./shared.js"; import { collectWhatsAppStatusIssues } from "./status-issues.js"; - -const meta = getChatChannelMeta("whatsapp"); - function normalizeWhatsAppPayloadText(text: string | undefined): string { return (text ?? "").replace(/^(?:[ \t]*\r?\n)+/, ""); } @@ -57,86 +41,16 @@ function parseWhatsAppExplicitTarget(raw: string) { } 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 +71,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 +123,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, @@ -297,7 +164,7 @@ export const whatsappPlugin: ChannelPlugin = { }, auth: { login: async ({ cfg, accountId, runtime, verbose }) => { - const resolvedAccountId = accountId?.trim() || resolveDefaultWhatsAppAccountId(cfg); + const resolvedAccountId = accountId?.trim() || whatsappPlugin.config.defaultAccountId(cfg); await ( await loadWhatsAppChannelRuntime() ).loginWeb(Boolean(verbose), undefined, runtime, resolvedAccountId); diff --git a/extensions/whatsapp/src/plugin-shared.ts b/extensions/whatsapp/src/plugin-shared.ts deleted file mode 100644 index fee78e620a4..00000000000 --- a/extensions/whatsapp/src/plugin-shared.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { ChannelPlugin } from "openclaw/plugin-sdk/whatsapp"; -import { type ResolvedWhatsAppAccount } from "./accounts.js"; - -export async function loadWhatsAppChannelRuntime() { - return await import("./channel.runtime.js"); -} - -export const whatsappSetupWizardProxy = { - channel: "whatsapp", - status: { - configuredLabel: "linked", - unconfiguredLabel: "not linked", - configuredHint: "linked", - unconfiguredHint: "not linked", - configuredScore: 5, - unconfiguredScore: 4, - resolveConfigured: async ({ cfg }) => - await ( - await loadWhatsAppChannelRuntime() - ).whatsappSetupWizard.status.resolveConfigured({ - cfg, - }), - resolveStatusLines: async ({ cfg, configured }) => - (await ( - await loadWhatsAppChannelRuntime() - ).whatsappSetupWizard.status.resolveStatusLines?.({ - cfg, - configured, - })) ?? [], - }, - resolveShouldPromptAccountIds: (params) => - (params.shouldPromptAccountIds || params.options?.promptWhatsAppAccountId) ?? false, - credentials: [], - finalize: async (params) => - await ( - await loadWhatsAppChannelRuntime() - ).whatsappSetupWizard.finalize!(params), - disable: (cfg) => ({ - ...cfg, - channels: { - ...cfg.channels, - whatsapp: { - ...cfg.channels?.whatsapp, - enabled: false, - }, - }, - }), - onAccountRecorded: (accountId, options) => { - options?.onWhatsAppAccountId?.(accountId); - }, -} satisfies NonNullable["setupWizard"]>; diff --git a/extensions/whatsapp/src/shared.ts b/extensions/whatsapp/src/shared.ts index 3a8f7412e7e..43df9bd7e6a 100644 --- a/extensions/whatsapp/src/shared.ts +++ b/extensions/whatsapp/src/shared.ts @@ -24,6 +24,14 @@ import { export const WHATSAPP_CHANNEL = "whatsapp" as const; +export async function loadWhatsAppChannelRuntime() { + return await import("./channel.runtime.js"); +} + +export const whatsappSetupWizardProxy = createWhatsAppSetupWizardProxy(async () => ({ + whatsappSetupWizard: (await loadWhatsAppChannelRuntime()).whatsappSetupWizard, +})); + export function createWhatsAppSetupWizardProxy( loadWizard: () => Promise<{ whatsappSetupWizard: NonNullable["setupWizard"]>;