From feac26c3b72e2fb7e8b10fc58ab5c550c814e4c6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 23:00:03 +0000 Subject: [PATCH] refactor: share allowFrom formatter scaffolding --- extensions/bluebubbles/src/channel.ts | 10 +++---- extensions/discord/src/channel.ts | 7 ++--- extensions/feishu/src/channel.ts | 11 ++++--- extensions/googlechat/src/channel.ts | 17 ++++++----- extensions/irc/src/channel.ts | 6 +++- extensions/mattermost/src/channel.ts | 6 +++- extensions/msteams/src/channel.ts | 11 ++++--- extensions/nextcloud-talk/src/channel.ts | 10 +++---- extensions/slack/src/channel.ts | 7 ++--- extensions/telegram/src/channel.ts | 7 ++--- src/plugin-sdk/allow-from.test.ts | 38 +++++++++++++++++++++++- src/plugin-sdk/allow-from.ts | 11 +++++++ src/plugin-sdk/index.ts | 1 + 13 files changed, 94 insertions(+), 48 deletions(-) diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index 69fdffc1bac..35b1e7c8c89 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -1,6 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyRestrictSendersWarnings, + formatNormalizedAllowFromEntries, } from "openclaw/plugin-sdk"; import type { ChannelAccountSnapshot, @@ -118,11 +119,10 @@ export const bluebubblesPlugin: ChannelPlugin = { String(entry), ), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^bluebubbles:/i, "")) - .map((entry) => normalizeBlueBubblesHandle(entry)), + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")), + }), }, actions: bluebubblesMessageActions, security: { diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index 43cdb441066..fa0f6f364ba 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -1,6 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyConfiguredRouteWarnings, + formatAllowFromLowercase, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -116,11 +117,7 @@ export const discordPlugin: ChannelPlugin = { (resolveDiscordAccount({ cfg, accountId }).config.dm?.allowFrom ?? []).map((entry) => String(entry), ), - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.toLowerCase()), + formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }), resolveDefaultTo: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined, }, diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 102c12f07d6..4e2782d0ba5 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -1,4 +1,7 @@ -import { collectOpenGroupPolicyRestrictSendersWarnings } from "openclaw/plugin-sdk"; +import { + collectOpenGroupPolicyRestrictSendersWarnings, + formatAllowFromLowercase, +} from "openclaw/plugin-sdk"; import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { buildProbeChannelStatusSummary, @@ -251,11 +254,7 @@ export const feishuPlugin: ChannelPlugin = { const account = resolveFeishuAccount({ cfg, accountId }); return (account.config?.allowFrom ?? []).map((entry) => String(entry)); }, - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.toLowerCase()), + formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }), }, security: { collectWarnings: ({ cfg, accountId }) => { diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index 021cdfe9cca..68f36276de0 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -1,6 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyConfigureRouteAllowlistWarning, + formatNormalizedAllowFromEntries, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -72,10 +73,10 @@ export const googlechatDock: ChannelDock = { String(entry), ), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry)) - .filter(Boolean) - .map(formatAllowFromEntry), + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: formatAllowFromEntry, + }), }, groups: { resolveRequireMention: resolveGoogleChatGroupRequireMention, @@ -183,10 +184,10 @@ export const googlechatPlugin: ChannelPlugin = { }).config.dm?.allowFrom ?? [] ).map((entry) => String(entry)), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry)) - .filter(Boolean) - .map(formatAllowFromEntry), + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: formatAllowFromEntry, + }), resolveDefaultTo: ({ cfg, accountId }) => resolveGoogleChatAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined, }, diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index bb3dae50266..2c389d7bc31 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -1,6 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, buildOpenGroupPolicyWarning, + formatNormalizedAllowFromEntries, } from "openclaw/plugin-sdk"; import { buildBaseAccountStatusSnapshot, @@ -118,7 +119,10 @@ export const ircPlugin: ChannelPlugin = { (entry) => String(entry), ), formatAllowFrom: ({ allowFrom }) => - allowFrom.map((entry) => normalizeIrcAllowEntry(String(entry))).filter(Boolean), + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: normalizeIrcAllowEntry, + }), resolveDefaultTo: ({ cfg, accountId }) => resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.defaultTo?.trim() || undefined, diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 073e64faddf..f7774173d50 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -1,6 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyRestrictSendersWarnings, + formatNormalizedAllowFromEntries, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -279,7 +280,10 @@ export const mattermostPlugin: ChannelPlugin = { String(entry), ), formatAllowFrom: ({ allowFrom }) => - allowFrom.map((entry) => formatAllowEntry(String(entry))).filter(Boolean), + formatNormalizedAllowFromEntries({ + allowFrom, + normalizeEntry: formatAllowEntry, + }), }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 7e2786a7279..75bcfbfe463 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -1,4 +1,7 @@ -import { collectOpenGroupPolicyRestrictSendersWarnings } from "openclaw/plugin-sdk"; +import { + collectOpenGroupPolicyRestrictSendersWarnings, + formatAllowFromLowercase, +} from "openclaw/plugin-sdk"; import type { ChannelMessageActionName, ChannelPlugin, @@ -126,11 +129,7 @@ export const msteamsPlugin: ChannelPlugin = { configured: account.configured, }), resolveAllowFrom: ({ cfg }) => cfg.channels?.msteams?.allowFrom ?? [], - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.toLowerCase()), + formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }), resolveDefaultTo: ({ cfg }) => cfg.channels?.msteams?.defaultTo?.trim() || undefined, }, security: { diff --git a/extensions/nextcloud-talk/src/channel.ts b/extensions/nextcloud-talk/src/channel.ts index 1f48e00e8ea..4d200e57a58 100644 --- a/extensions/nextcloud-talk/src/channel.ts +++ b/extensions/nextcloud-talk/src/channel.ts @@ -1,6 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyRouteAllowlistWarnings, + formatAllowFromLowercase, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -112,11 +113,10 @@ export const nextcloudTalkPlugin: ChannelPlugin = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? [] ).map((entry) => String(entry).toLowerCase()), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^(nextcloud-talk|nc-talk|nc):/i, "")) - .map((entry) => entry.toLowerCase()), + formatAllowFromLowercase({ + allowFrom, + stripPrefixRe: /^(nextcloud-talk|nc-talk|nc):/i, + }), }, security: { resolveDmPolicy: ({ cfg, accountId, account }) => { diff --git a/extensions/slack/src/channel.ts b/extensions/slack/src/channel.ts index 8c695e98fa2..831c5c386d3 100644 --- a/extensions/slack/src/channel.ts +++ b/extensions/slack/src/channel.ts @@ -1,6 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyConfiguredRouteWarnings, + formatAllowFromLowercase, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -166,11 +167,7 @@ export const slackPlugin: ChannelPlugin = { }), resolveAllowFrom: ({ cfg, accountId }) => (resolveSlackAccount({ cfg, accountId }).dm?.allowFrom ?? []).map((entry) => String(entry)), - formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.toLowerCase()), + formatAllowFrom: ({ allowFrom }) => formatAllowFromLowercase({ allowFrom }), resolveDefaultTo: ({ cfg, accountId }) => resolveSlackAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined, }, diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 90ed9aadf24..ed4c4a6bd99 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -1,6 +1,7 @@ import { buildAccountScopedDmSecurityPolicy, collectOpenGroupPolicyRouteAllowlistWarnings, + formatAllowFromLowercase, } from "openclaw/plugin-sdk"; import { applyAccountNameToChannelSection, @@ -179,11 +180,7 @@ export const telegramPlugin: ChannelPlugin - allowFrom - .map((entry) => String(entry).trim()) - .filter(Boolean) - .map((entry) => entry.replace(/^(telegram|tg):/i, "")) - .map((entry) => entry.toLowerCase()), + formatAllowFromLowercase({ allowFrom, stripPrefixRe: /^(telegram|tg):/i }), resolveDefaultTo: ({ cfg, accountId }) => { const val = resolveTelegramAccount({ cfg, accountId }).config.defaultTo; return val != null ? String(val) : undefined; diff --git a/src/plugin-sdk/allow-from.test.ts b/src/plugin-sdk/allow-from.test.ts index 8ad13fe98f6..f2c5d681559 100644 --- a/src/plugin-sdk/allow-from.test.ts +++ b/src/plugin-sdk/allow-from.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { isAllowedParsedChatSender, isNormalizedSenderAllowed } from "./allow-from.js"; +import { + formatAllowFromLowercase, + formatNormalizedAllowFromEntries, + isAllowedParsedChatSender, + isNormalizedSenderAllowed, +} from "./allow-from.js"; function parseAllowTarget( entry: string, @@ -102,3 +107,34 @@ describe("isNormalizedSenderAllowed", () => { ).toBe(false); }); }); + +describe("formatAllowFromLowercase", () => { + it("trims, strips prefixes, and lowercases entries", () => { + expect( + formatAllowFromLowercase({ + allowFrom: [" Telegram:UserA ", "tg:UserB", " "], + stripPrefixRe: /^(telegram|tg):/i, + }), + ).toEqual(["usera", "userb"]); + }); +}); + +describe("formatNormalizedAllowFromEntries", () => { + it("applies custom normalization after trimming", () => { + expect( + formatNormalizedAllowFromEntries({ + allowFrom: [" @Alice ", "", " @Bob "], + normalizeEntry: (entry) => entry.replace(/^@/, "").toLowerCase(), + }), + ).toEqual(["alice", "bob"]); + }); + + it("filters empty normalized entries", () => { + expect( + formatNormalizedAllowFromEntries({ + allowFrom: ["@", "valid"], + normalizeEntry: (entry) => entry.replace(/^@$/, ""), + }), + ).toEqual(["valid"]); + }); +}); diff --git a/src/plugin-sdk/allow-from.ts b/src/plugin-sdk/allow-from.ts index 93c3d52c712..9b43a8ced6d 100644 --- a/src/plugin-sdk/allow-from.ts +++ b/src/plugin-sdk/allow-from.ts @@ -9,6 +9,17 @@ export function formatAllowFromLowercase(params: { .map((entry) => entry.toLowerCase()); } +export function formatNormalizedAllowFromEntries(params: { + allowFrom: Array; + normalizeEntry: (entry: string) => string | undefined | null; +}): string[] { + return params.allowFrom + .map((entry) => String(entry).trim()) + .filter(Boolean) + .map((entry) => params.normalizeEntry(entry)) + .filter((entry): entry is string => Boolean(entry)); +} + export function isNormalizedSenderAllowed(params: { senderId: string | number; allowFrom: Array; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 8f25e5c8aed..aeee7760445 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -274,6 +274,7 @@ export { } from "../routing/session-key.js"; export { formatAllowFromLowercase, + formatNormalizedAllowFromEntries, isAllowedParsedChatSender, isNormalizedSenderAllowed, } from "./allow-from.js";