diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 7c32c8df604..4a05b64fb1a 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -1,9 +1,8 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyConfiguredRouteWarnings, + createScopedAccountConfigAccessors, formatAllowFromLowercase, - mapAllowFromEntries, - resolveOptionalConfigString, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -58,6 +57,13 @@ const discordMessageActions: ChannelMessageActionAdapter = { }, }; +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, +}); + export const discordPlugin: ChannelPlugin = { id: "discord", meta: { @@ -115,11 +121,7 @@ export const discordPlugin: ChannelPlugin = { configured: Boolean(account.token?.trim()), tokenSource: account.tokenSource, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries(resolveDiscordAccount({ cfg, accountId }).config.dm?.allowFrom), - formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }), - resolveDefaultTo: ({ cfg, accountId }) => - resolveOptionalConfigString(resolveDiscordAccount({ cfg, accountId }).config.defaultTo), + ...discordConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index accfed59808..9b4d5687979 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -1,9 +1,8 @@ import { buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyConfigureRouteAllowlistWarning, + createScopedAccountConfigAccessors, formatNormalizedAllowFromEntries, - mapAllowFromEntries, - resolveOptionalConfigString, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -59,6 +58,17 @@ const formatAllowFromEntry = (entry: string) => .replace(/^users\//i, "") .toLowerCase(); +const googleChatConfigAccessors = createScopedAccountConfigAccessors({ + resolveAccount: ({ cfg, accountId }) => resolveGoogleChatAccount({ cfg, accountId }), + resolveAllowFrom: (account: ResolvedGoogleChatAccount) => account.config.dm?.allowFrom, + formatAllowFrom: (allowFrom) => + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: formatAllowFromEntry, + }), + resolveDefaultTo: (account: ResolvedGoogleChatAccount) => account.config.defaultTo, +}); + export const googlechatDock: ChannelDock = { id: "googlechat", capabilities: { @@ -69,15 +79,7 @@ export const googlechatDock: ChannelDock = { blockStreaming: true, }, outbound: { textChunkLimit: 4000 }, - config: { - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries(resolveGoogleChatAccount({ cfg: cfg, accountId }).config.dm?.allowFrom), - formatAllowFrom: ({ allowFrom }) => - formatNormalizedAllowFromEntries({ - allowFrom, - normalizeEntry: formatAllowFromEntry, - }), - }, + config: googleChatConfigAccessors, groups: { resolveRequireMention: resolveGoogleChatGroupRequireMention, }, @@ -176,20 +178,7 @@ export const googlechatPlugin: ChannelPlugin = { configured: account.credentialSource !== "none", credentialSource: account.credentialSource, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries( - resolveGoogleChatAccount({ - cfg: cfg, - accountId, - }).config.dm?.allowFrom, - ), - formatAllowFrom: ({ allowFrom }) => - formatNormalizedAllowFromEntries({ - allowFrom, - normalizeEntry: formatAllowFromEntry, - }), - resolveDefaultTo: ({ cfg, accountId }) => - resolveOptionalConfigString(resolveGoogleChatAccount({ cfg, accountId }).config.defaultTo), + ...googleChatConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index d7ecd0fab2d..c5b2ed07014 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -1,9 +1,8 @@ import { buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyWarning, + createScopedAccountConfigAccessors, formatNormalizedAllowFromEntries, - mapAllowFromEntries, - resolveOptionalConfigString, } from "openclaw/plugin-sdk"; import { buildBaseAccountStatusSnapshot, @@ -49,6 +48,17 @@ function normalizePairingTarget(raw: string): string { return normalized.split(/[!@]/, 1)[0]?.trim() ?? ""; } +const ircConfigAccessors = createScopedAccountConfigAccessors({ + resolveAccount: ({ cfg, accountId }) => resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }), + resolveAllowFrom: (account: ResolvedIrcAccount) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: normalizeIrcAllowEntry, + }), + resolveDefaultTo: (account: ResolvedIrcAccount) => account.config.defaultTo, +}); + export const ircPlugin: ChannelPlugin = { id: "irc", meta: { @@ -116,19 +126,7 @@ export const ircPlugin: ChannelPlugin = { nick: account.nick, passwordSource: account.passwordSource, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries( - resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom, - ), - formatAllowFrom: ({ allowFrom }) => - formatNormalizedAllowFromEntries({ - allowFrom, - normalizeEntry: normalizeIrcAllowEntry, - }), - resolveDefaultTo: ({ cfg, accountId }) => - resolveOptionalConfigString( - resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.defaultTo, - ), + ...ircConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index 93cb3a41b5d..57cd8eacc1c 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -1,7 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyRestrictSendersWarnings, - mapAllowFromEntries, + createScopedAccountConfigAccessors, } from "openclaw/plugin-sdk"; import { buildChannelConfigSchema, @@ -34,6 +34,17 @@ const meta = { systemImage: "message.fill", }; +const lineConfigAccessors = createScopedAccountConfigAccessors({ + resolveAccount: ({ cfg, accountId }) => + getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }), + resolveAllowFrom: (account: ResolvedLineAccount) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + allowFrom + .map((entry) => String(entry).trim()) + .filter(Boolean) + .map((entry) => entry.replace(/^line:(?:user:)?/i, "")), +}); + function patchLineAccountConfig( cfg: OpenClawConfig, lineConfig: LineConfig, @@ -147,19 +158,7 @@ export const linePlugin: ChannelPlugin = { configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()), tokenSource: account.tokenSource ?? undefined, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries( - getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }) - .config.allowFrom, - ), - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => { - // LINE sender IDs are case-sensitive; keep original casing. - return entry.replace(/^line:(?:user:)?/i, ""); - }), + ...lineConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/matrix/src/channel.ts b/extensions/matrix/src/channel.ts index 3e1d0e5668c..a9b2a7c7f77 100644 --- a/extensions/matrix/src/channel.ts +++ b/extensions/matrix/src/channel.ts @@ -1,7 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyWarning, - mapAllowFromEntries, + createScopedAccountConfigAccessors, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -100,6 +100,13 @@ function buildMatrixConfigUpdate( }; } +const matrixConfigAccessors = createScopedAccountConfigAccessors({ + resolveAccount: ({ cfg, accountId }) => + resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }), + resolveAllowFrom: (account) => account.dm?.allowFrom, + formatAllowFrom: (allowFrom) => normalizeMatrixAllowList(allowFrom), +}); + export const matrixPlugin: ChannelPlugin = { id: "matrix", meta, @@ -155,11 +162,7 @@ export const matrixPlugin: ChannelPlugin = { configured: account.configured, baseUrl: account.homeserver, }), - resolveAllowFrom: ({ cfg, accountId }) => { - const matrixConfig = resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }); - return mapAllowFromEntries(matrixConfig.dm?.allowFrom); - }, - formatAllowFrom: ({ allowFrom }) => normalizeMatrixAllowList(allowFrom), + ...matrixConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 320a1c24c34..b70cee76e46 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -1,8 +1,8 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyRestrictSendersWarnings, + createScopedAccountConfigAccessors, formatNormalizedAllowFromEntries, - mapAllowFromEntries, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -223,6 +223,16 @@ function formatAllowEntry(entry: string): string { return trimmed.replace(/^(mattermost|user):/i, "").toLowerCase(); } +const mattermostConfigAccessors = createScopedAccountConfigAccessors({ + resolveAccount: ({ cfg, accountId }) => resolveMattermostAccount({ cfg, accountId }), + resolveAllowFrom: (account: ResolvedMattermostAccount) => account.config.allowFrom, + formatAllowFrom: (allowFrom) => + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: formatAllowEntry, + }), +}); + export const mattermostPlugin: ChannelPlugin = { id: "mattermost", meta: { @@ -276,13 +286,7 @@ export const mattermostPlugin: ChannelPlugin = { botTokenSource: account.botTokenSource, baseUrl: account.baseUrl, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries(resolveMattermostAccount({ cfg, accountId }).config.allowFrom), - formatAllowFrom: ({ allowFrom }) => - formatNormalizedAllowFromEntries({ - allowFrom, - normalizeEntry: formatAllowEntry, - }), + ...mattermostConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index e204d2a0ac9..9dc2268a3b0 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -1,8 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyRestrictSendersWarnings, - mapAllowFromEntries, - resolveOptionalConfigString, + createScopedAccountConfigAccessors, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -50,6 +49,18 @@ const signalMessageActions: ChannelMessageActionAdapter = { const meta = getChatChannelMeta("signal"); +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, +}); + function buildSignalSetupPatch(input: { signalNumber?: string; cliPath?: string; @@ -144,16 +155,7 @@ export const signalPlugin: ChannelPlugin = { configured: account.configured, baseUrl: account.baseUrl, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries(resolveSignalAccount({ cfg, accountId }).config.allowFrom), - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, "")))) - .filter(Boolean), - resolveDefaultTo: ({ cfg, accountId }) => - resolveOptionalConfigString(resolveSignalAccount({ cfg, accountId }).config.defaultTo), + ...signalConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 31dc02efaff..ec2faf17744 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -1,9 +1,8 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyConfiguredRouteWarnings, + createScopedAccountConfigAccessors, formatAllowFromLowercase, - mapAllowFromEntries, - resolveOptionalConfigString, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -91,6 +90,13 @@ function resolveSlackSendContext(params: { return { send, threadTsValue, tokenOverride }; } +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, +}); + export const slackPlugin: ChannelPlugin = { id: "slack", meta: { @@ -167,11 +173,7 @@ export const slackPlugin: ChannelPlugin = { botTokenSource: account.botTokenSource, appTokenSource: account.appTokenSource, }), - resolveAllowFrom: ({ cfg, accountId }) => - mapAllowFromEntries(resolveSlackAccount({ cfg, accountId }).dm?.allowFrom), - formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }), - resolveDefaultTo: ({ cfg, accountId }) => - resolveOptionalConfigString(resolveSlackAccount({ cfg, accountId }).config.defaultTo), + ...slackConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index afec02f110e..bc492e9754b 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -1,9 +1,8 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyRouteAllowlistWarnings, + createScopedAccountConfigAccessors, formatAllowFromLowercase, - mapAllowFromEntries, - resolveOptionalConfigString, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -94,6 +93,14 @@ const telegramMessageActions: ChannelMessageActionAdapter = { }, }; +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, +}); + export const telegramPlugin: ChannelPlugin = { id: "telegram", meta: { @@ -177,12 +184,7 @@ export const telegramPlugin: ChannelPlugin - mapAllowFromEntries(resolveTelegramAccount({ cfg, accountId }).config.allowFrom), - formatAllowFrom: ({ allowFrom }) => - formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(telegram|tg):/i }), - resolveDefaultTo: ({ cfg, accountId }) => - resolveOptionalConfigString(resolveTelegramAccount({ cfg, accountId }).config.defaultTo), + ...telegramConfigAccessors, }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/src/plugin-sdk/channel-config-helpers.test.ts b/src/plugin-sdk/channel-config-helpers.test.ts index 901acd3f6bb..1f81bc0d744 100644 --- a/src/plugin-sdk/channel-config-helpers.test.ts +++ b/src/plugin-sdk/channel-config-helpers.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { mapAllowFromEntries, resolveOptionalConfigString } from "./channel-config-helpers.js"; +import { + createScopedAccountConfigAccessors, + mapAllowFromEntries, + resolveOptionalConfigString, +} from "./channel-config-helpers.js"; describe("mapAllowFromEntries", () => { it("coerces allowFrom entries to strings", () => { @@ -25,3 +29,46 @@ describe("resolveOptionalConfigString", () => { expect(resolveOptionalConfigString(undefined)).toBeUndefined(); }); }); + +describe("createScopedAccountConfigAccessors", () => { + it("maps allowFrom and defaultTo from the resolved account", () => { + const accessors = createScopedAccountConfigAccessors({ + resolveAccount: ({ accountId }) => ({ + allowFrom: accountId ? [accountId, 42] : ["fallback"], + defaultTo: " room:123 ", + }), + resolveAllowFrom: (account) => account.allowFrom, + formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry).toUpperCase()), + resolveDefaultTo: (account) => account.defaultTo, + }); + + expect( + accessors.resolveAllowFrom?.({ + cfg: {}, + accountId: "owner", + }), + ).toEqual(["owner", "42"]); + expect( + accessors.formatAllowFrom?.({ + cfg: {}, + allowFrom: ["owner"], + }), + ).toEqual(["OWNER"]); + expect( + accessors.resolveDefaultTo?.({ + cfg: {}, + accountId: "owner", + }), + ).toBe("room:123"); + }); + + it("omits resolveDefaultTo when no selector is provided", () => { + const accessors = createScopedAccountConfigAccessors({ + resolveAccount: () => ({ allowFrom: ["owner"] }), + resolveAllowFrom: (account) => account.allowFrom, + formatAllowFrom: (allowFrom) => allowFrom.map((entry) => String(entry)), + }); + + expect(accessors.resolveDefaultTo).toBeUndefined(); + }); +}); diff --git a/src/plugin-sdk/channel-config-helpers.ts b/src/plugin-sdk/channel-config-helpers.ts index d5baa765df6..754e2a57c1a 100644 --- a/src/plugin-sdk/channel-config-helpers.ts +++ b/src/plugin-sdk/channel-config-helpers.ts @@ -1,4 +1,5 @@ import { normalizeWhatsAppAllowFromEntries } from "../channels/plugins/normalize/whatsapp.js"; +import type { ChannelConfigAdapter } from "../channels/plugins/types.adapters.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveIMessageAccount } from "../imessage/accounts.js"; import { normalizeAccountId } from "../routing/session-key.js"; @@ -25,6 +26,35 @@ export function resolveOptionalConfigString( return normalized || undefined; } +export function createScopedAccountConfigAccessors(params: { + resolveAccount: (params: { cfg: OpenClawConfig; accountId?: string | null }) => ResolvedAccount; + resolveAllowFrom: (account: ResolvedAccount) => Array | null | undefined; + formatAllowFrom: (allowFrom: Array) => string[]; + resolveDefaultTo?: (account: ResolvedAccount) => string | number | null | undefined; +}): Pick< + ChannelConfigAdapter, + "resolveAllowFrom" | "formatAllowFrom" | "resolveDefaultTo" +> { + const base = { + resolveAllowFrom: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) => + mapAllowFromEntries(params.resolveAllowFrom(params.resolveAccount({ cfg, accountId }))), + formatAllowFrom: ({ allowFrom }: { allowFrom: Array }) => + params.formatAllowFrom(allowFrom), + }; + + if (!params.resolveDefaultTo) { + return base; + } + + return { + ...base, + resolveDefaultTo: ({ cfg, accountId }) => + resolveOptionalConfigString( + params.resolveDefaultTo?.(params.resolveAccount({ cfg, accountId })), + ), + }; +} + export function resolveWhatsAppConfigAllowFrom(params: { cfg: OpenClawConfig; accountId?: string | null; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index d9b8b035971..68da9afb8e1 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -380,6 +380,7 @@ export type { ChunkMode } from "../auto-reply/chunk.js"; export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js"; export { formatInboundFromLabel } from "../auto-reply/envelope.js"; export { + createScopedAccountConfigAccessors, formatTrimmedAllowFromEntries, mapAllowFromEntries, resolveOptionalConfigString,