refactor(plugins): split lightweight channel setup modules

This commit is contained in:
Peter Steinberger
2026-03-15 19:38:13 -07:00
parent 88b8151c52
commit b580d142cd
12 changed files with 721 additions and 12 deletions

View File

@@ -1,3 +1,3 @@
import { discordPlugin } from "./src/channel.js";
import { discordSetupPlugin } from "./src/channel.setup.js";
export default { plugin: discordPlugin };
export default { plugin: discordSetupPlugin };

View File

@@ -0,0 +1,75 @@
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
import {
createScopedAccountConfigAccessors,
formatAllowFromLowercase,
} from "openclaw/plugin-sdk/compat";
import {
buildChannelConfigSchema,
DiscordConfigSchema,
getChatChannelMeta,
inspectDiscordAccount,
listDiscordAccountIds,
resolveDefaultDiscordAccountId,
resolveDiscordAccount,
type ChannelPlugin,
type ResolvedDiscordAccount,
} from "openclaw/plugin-sdk/discord";
import { createDiscordSetupWizardProxy, discordSetupAdapter } from "./setup-core.js";
async function loadDiscordChannelRuntime() {
return await import("./channel.runtime.js");
}
const discordConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
});
const discordConfigBase = createScopedChannelConfigBase({
sectionKey: "discord",
listAccountIds: listDiscordAccountIds,
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultDiscordAccountId,
clearBaseFields: ["token", "name"],
});
const discordSetupWizard = createDiscordSetupWizardProxy(async () => ({
discordSetupWizard: (await loadDiscordChannelRuntime()).discordSetupWizard,
}));
export const discordSetupPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
id: "discord",
meta: {
...getChatChannelMeta("discord"),
},
setupWizard: discordSetupWizard,
capabilities: {
chatTypes: ["direct", "channel", "thread"],
polls: true,
reactions: true,
threads: true,
media: true,
nativeCommands: true,
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.discord"] },
configSchema: buildChannelConfigSchema(DiscordConfigSchema),
config: {
...discordConfigBase,
isConfigured: (account) => Boolean(account.token?.trim()),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.token?.trim()),
tokenSource: account.tokenSource,
}),
...discordConfigAccessors,
},
setup: discordSetupAdapter,
};

View File

@@ -1,3 +1,3 @@
import { imessagePlugin } from "./src/channel.js";
import { imessageSetupPlugin } from "./src/channel.setup.js";
export default { plugin: imessagePlugin };
export default { plugin: imessageSetupPlugin };

View File

@@ -0,0 +1,99 @@
import {
buildAccountScopedDmSecurityPolicy,
collectAllowlistProviderRestrictSendersWarnings,
} from "openclaw/plugin-sdk/compat";
import {
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
formatTrimmedAllowFromEntries,
getChatChannelMeta,
IMessageConfigSchema,
listIMessageAccountIds,
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
resolveIMessageConfigAllowFrom,
resolveIMessageConfigDefaultTo,
setAccountEnabledInConfigSection,
type ChannelPlugin,
type ResolvedIMessageAccount,
} from "openclaw/plugin-sdk/imessage";
import { createIMessageSetupWizardProxy, imessageSetupAdapter } from "./setup-core.js";
async function loadIMessageChannelRuntime() {
return await import("./channel.runtime.js");
}
const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({
imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard,
}));
export const imessageSetupPlugin: ChannelPlugin<ResolvedIMessageAccount> = {
id: "imessage",
meta: {
...getChatChannelMeta("imessage"),
aliases: ["imsg"],
showConfigured: false,
},
setupWizard: imessageSetupWizard,
capabilities: {
chatTypes: ["direct", "group"],
media: true,
},
reload: { configPrefixes: ["channels.imessage"] },
configSchema: buildChannelConfigSchema(IMessageConfigSchema),
config: {
listAccountIds: (cfg) => listIMessageAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultIMessageAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg,
sectionKey: "imessage",
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg,
sectionKey: "imessage",
accountId,
clearBaseFields: ["cliPath", "dbPath", "service", "region", "name"],
}),
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
}),
resolveAllowFrom: ({ cfg, accountId }) => resolveIMessageConfigAllowFrom({ cfg, accountId }),
formatAllowFrom: ({ allowFrom }) => formatTrimmedAllowFromEntries(allowFrom),
resolveDefaultTo: ({ cfg, accountId }) => resolveIMessageConfigDefaultTo({ cfg, accountId }),
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) =>
buildAccountScopedDmSecurityPolicy({
cfg,
channelKey: "imessage",
accountId,
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
policy: account.config.dmPolicy,
allowFrom: account.config.allowFrom ?? [],
policyPathSuffix: "dmPolicy",
}),
collectWarnings: ({ account, cfg }) =>
collectAllowlistProviderRestrictSendersWarnings({
cfg,
providerConfigPresent: cfg.channels?.imessage !== undefined,
configuredGroupPolicy: account.config.groupPolicy,
surface: "iMessage groups",
openScope: "any member",
groupPolicyPath: "channels.imessage.groupPolicy",
groupAllowFromPath: "channels.imessage.groupAllowFrom",
mentionGated: false,
}),
},
setup: imessageSetupAdapter,
};

View File

@@ -1,3 +1,3 @@
import { signalPlugin } from "./src/channel.js";
import { signalSetupPlugin } from "./src/channel.setup.js";
export default { plugin: signalPlugin };
export default { plugin: signalSetupPlugin };

View File

@@ -0,0 +1,112 @@
import {
createScopedAccountConfigAccessors,
buildAccountScopedDmSecurityPolicy,
collectAllowlistProviderRestrictSendersWarnings,
} from "openclaw/plugin-sdk/compat";
import {
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
deleteAccountFromConfigSection,
getChatChannelMeta,
listSignalAccountIds,
normalizeE164,
resolveDefaultSignalAccountId,
resolveSignalAccount,
setAccountEnabledInConfigSection,
SignalConfigSchema,
type ChannelPlugin,
type ResolvedSignalAccount,
} from "openclaw/plugin-sdk/signal";
import { createSignalSetupWizardProxy, signalSetupAdapter } from "./setup-core.js";
async function loadSignalChannelRuntime() {
return await import("./channel.runtime.js");
}
const signalSetupWizard = createSignalSetupWizardProxy(async () => ({
signalSetupWizard: (await loadSignalChannelRuntime()).signalSetupWizard,
}));
const signalConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveSignalAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedSignalAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
.filter(Boolean),
resolveDefaultTo: (account: ResolvedSignalAccount) => account.config.defaultTo,
});
export const signalSetupPlugin: ChannelPlugin<ResolvedSignalAccount> = {
id: "signal",
meta: {
...getChatChannelMeta("signal"),
},
setupWizard: signalSetupWizard,
capabilities: {
chatTypes: ["direct", "group"],
media: true,
reactions: true,
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.signal"] },
configSchema: buildChannelConfigSchema(SignalConfigSchema),
config: {
listAccountIds: (cfg) => listSignalAccountIds(cfg),
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg,
sectionKey: "signal",
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg,
sectionKey: "signal",
accountId,
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
}),
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: account.configured,
baseUrl: account.baseUrl,
}),
...signalConfigAccessors,
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) =>
buildAccountScopedDmSecurityPolicy({
cfg,
channelKey: "signal",
accountId,
fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
policy: account.config.dmPolicy,
allowFrom: account.config.allowFrom ?? [],
policyPathSuffix: "dmPolicy",
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
}),
collectWarnings: ({ account, cfg }) =>
collectAllowlistProviderRestrictSendersWarnings({
cfg,
providerConfigPresent: cfg.channels?.signal !== undefined,
configuredGroupPolicy: account.config.groupPolicy,
surface: "Signal groups",
openScope: "any member",
groupPolicyPath: "channels.signal.groupPolicy",
groupAllowFromPath: "channels.signal.groupAllowFrom",
mentionGated: false,
}),
},
setup: signalSetupAdapter,
};

View File

@@ -1,3 +1,3 @@
import { slackPlugin } from "./src/channel.js";
import { slackSetupPlugin } from "./src/channel.setup.js";
export default { plugin: slackPlugin };
export default { plugin: slackSetupPlugin };

View File

@@ -0,0 +1,100 @@
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
import {
createScopedAccountConfigAccessors,
formatAllowFromLowercase,
} from "openclaw/plugin-sdk/compat";
import {
buildChannelConfigSchema,
getChatChannelMeta,
inspectSlackAccount,
listSlackAccountIds,
resolveDefaultSlackAccountId,
resolveSlackAccount,
SlackConfigSchema,
type ChannelPlugin,
type ResolvedSlackAccount,
} from "openclaw/plugin-sdk/slack";
import { createSlackSetupWizardProxy, slackSetupAdapter } from "./setup-core.js";
async function loadSlackChannelRuntime() {
return await import("./channel.runtime.js");
}
function isSlackAccountConfigured(account: ResolvedSlackAccount): boolean {
const mode = account.config.mode ?? "socket";
const hasBotToken = Boolean(account.botToken?.trim());
if (!hasBotToken) {
return false;
}
if (mode === "http") {
return Boolean(account.config.signingSecret?.trim());
}
return Boolean(account.appToken?.trim());
}
const slackConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveSlackAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedSlackAccount) => account.dm?.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account: ResolvedSlackAccount) => account.config.defaultTo,
});
const slackConfigBase = createScopedChannelConfigBase({
sectionKey: "slack",
listAccountIds: listSlackAccountIds,
resolveAccount: (cfg, accountId) => resolveSlackAccount({ cfg, accountId }),
inspectAccount: (cfg, accountId) => inspectSlackAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultSlackAccountId,
clearBaseFields: ["botToken", "appToken", "name"],
});
const slackSetupWizard = createSlackSetupWizardProxy(async () => ({
slackSetupWizard: (await loadSlackChannelRuntime()).slackSetupWizard,
}));
export const slackSetupPlugin: ChannelPlugin<ResolvedSlackAccount> = {
id: "slack",
meta: {
...getChatChannelMeta("slack"),
preferSessionLookupForAnnounceTarget: true,
},
setupWizard: slackSetupWizard,
capabilities: {
chatTypes: ["direct", "channel", "thread"],
reactions: true,
threads: true,
media: true,
nativeCommands: true,
},
agentPrompt: {
messageToolHints: ({ cfg, accountId }) =>
cfg.channels?.slack?.accounts?.[accountId ?? "default"]?.capabilities?.interactiveReplies ===
true || cfg.channels?.slack?.capabilities?.interactiveReplies === true
? [
"- Slack interactive replies: use `[[slack_buttons: Label:value, Other:other]]` to add action buttons that route clicks back as Slack interaction system events.",
"- Slack selects: use `[[slack_select: Placeholder | Label:value, Other:other]]` to add a static select menu that routes the chosen value back as a Slack interaction system event.",
]
: [
"- Slack interactive replies are disabled. If needed, ask to set `channels.slack.capabilities.interactiveReplies=true` (or the same under `channels.slack.accounts.<account>.capabilities`).",
],
},
streaming: {
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
},
reload: { configPrefixes: ["channels.slack"] },
configSchema: buildChannelConfigSchema(SlackConfigSchema),
config: {
...slackConfigBase,
isConfigured: (account) => isSlackAccountConfigured(account),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: isSlackAccountConfigured(account),
botTokenSource: account.botTokenSource,
appTokenSource: account.appTokenSource,
}),
...slackConfigAccessors,
},
setup: slackSetupAdapter,
};

View File

@@ -1,3 +1,3 @@
import { telegramPlugin } from "./src/channel.js";
import { telegramSetupPlugin } from "./src/channel.setup.js";
export default { plugin: telegramPlugin };
export default { plugin: telegramSetupPlugin };

View File

@@ -0,0 +1,125 @@
import { createScopedChannelConfigBase } from "openclaw/plugin-sdk/compat";
import {
createScopedAccountConfigAccessors,
formatAllowFromLowercase,
} from "openclaw/plugin-sdk/compat";
import {
buildChannelConfigSchema,
getChatChannelMeta,
inspectTelegramAccount,
listTelegramAccountIds,
normalizeAccountId,
resolveDefaultTelegramAccountId,
resolveTelegramAccount,
TelegramConfigSchema,
type ChannelPlugin,
type OpenClawConfig,
type ResolvedTelegramAccount,
type TelegramProbe,
} from "openclaw/plugin-sdk/telegram";
import { telegramSetupAdapter } from "./setup-core.js";
import { telegramSetupWizard } from "./setup-surface.js";
function findTelegramTokenOwnerAccountId(params: {
cfg: OpenClawConfig;
accountId: string;
}): string | null {
const normalizedAccountId = normalizeAccountId(params.accountId);
const tokenOwners = new Map<string, string>();
for (const id of listTelegramAccountIds(params.cfg)) {
const account = inspectTelegramAccount({ cfg: params.cfg, accountId: id });
const token = (account.token ?? "").trim();
if (!token) {
continue;
}
const ownerAccountId = tokenOwners.get(token);
if (!ownerAccountId) {
tokenOwners.set(token, account.accountId);
continue;
}
if (account.accountId === normalizedAccountId) {
return ownerAccountId;
}
}
return null;
}
function formatDuplicateTelegramTokenReason(params: {
accountId: string;
ownerAccountId: string;
}): string {
return (
`Duplicate Telegram bot token: account "${params.accountId}" shares a token with ` +
`account "${params.ownerAccountId}". Keep one owner account per bot token.`
);
}
const telegramConfigAccessors = createScopedAccountConfigAccessors({
resolveAccount: ({ cfg, accountId }) => resolveTelegramAccount({ cfg, accountId }),
resolveAllowFrom: (account: ResolvedTelegramAccount) => account.config.allowFrom,
formatAllowFrom: (allowFrom) =>
formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(telegram|tg):/i }),
resolveDefaultTo: (account: ResolvedTelegramAccount) => account.config.defaultTo,
});
const telegramConfigBase = createScopedChannelConfigBase<ResolvedTelegramAccount>({
sectionKey: "telegram",
listAccountIds: listTelegramAccountIds,
resolveAccount: (cfg, accountId) => resolveTelegramAccount({ cfg, accountId }),
inspectAccount: (cfg, accountId) => inspectTelegramAccount({ cfg, accountId }),
defaultAccountId: resolveDefaultTelegramAccountId,
clearBaseFields: ["botToken", "tokenFile", "name"],
});
export const telegramSetupPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProbe> = {
id: "telegram",
meta: {
...getChatChannelMeta("telegram"),
quickstartAllowFrom: true,
},
setupWizard: telegramSetupWizard,
capabilities: {
chatTypes: ["direct", "group", "channel", "thread"],
reactions: true,
threads: true,
media: true,
polls: true,
nativeCommands: true,
blockStreaming: true,
},
reload: { configPrefixes: ["channels.telegram"] },
configSchema: buildChannelConfigSchema(TelegramConfigSchema),
config: {
...telegramConfigBase,
isConfigured: (account, cfg) => {
if (!account.token?.trim()) {
return false;
}
return !findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId });
},
unconfiguredReason: (account, cfg) => {
if (!account.token?.trim()) {
return "not configured";
}
const ownerAccountId = findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId });
if (!ownerAccountId) {
return "not configured";
}
return formatDuplicateTelegramTokenReason({
accountId: account.accountId,
ownerAccountId,
});
},
describeAccount: (account, cfg) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured:
Boolean(account.token?.trim()) &&
!findTelegramTokenOwnerAccountId({ cfg, accountId: account.accountId }),
tokenSource: account.tokenSource,
}),
...telegramConfigAccessors,
},
setup: telegramSetupAdapter,
};

View File

@@ -1,3 +1,3 @@
import { whatsappPlugin } from "./src/channel.js";
import { whatsappSetupPlugin } from "./src/channel.setup.js";
export default { plugin: whatsappPlugin };
export default { plugin: whatsappSetupPlugin };

View File

@@ -0,0 +1,198 @@
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 { webAuthExists } from "./auth-store.js";
import { whatsappSetupAdapter } from "./setup-core.js";
async function loadWhatsAppChannelRuntime() {
return await import("./channel.runtime.js");
}
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<ChannelPlugin<ResolvedWhatsAppAccount>["setupWizard"]>;
export const whatsappSetupPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
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",
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,
},
};