From d55fa78e403a4255d1a31e28bbd879358dff4fed Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Mar 2026 00:51:14 +0000 Subject: [PATCH] refactor: share delimited channel entry parsing --- extensions/irc/src/accounts.test.ts | 26 ++++++++++++++++++++++++++ extensions/irc/src/accounts.ts | 16 ++++------------ src/channels/plugins/helpers.test.ts | 21 ++++++++++++++++++++- src/channels/plugins/helpers.ts | 11 +++++++++++ src/commands/channels/add.ts | 16 +++------------- src/plugin-sdk/irc.ts | 5 ++++- 6 files changed, 68 insertions(+), 27 deletions(-) diff --git a/extensions/irc/src/accounts.test.ts b/extensions/irc/src/accounts.test.ts index afd1b597b81..5b4685795c6 100644 --- a/extensions/irc/src/accounts.test.ts +++ b/extensions/irc/src/accounts.test.ts @@ -81,6 +81,32 @@ describe("resolveDefaultIrcAccountId", () => { }); describe("resolveIrcAccount", () => { + it("parses delimited IRC_CHANNELS env values for the default account", () => { + const previousChannels = process.env.IRC_CHANNELS; + process.env.IRC_CHANNELS = "alpha, beta\ngamma; delta"; + + try { + const account = resolveIrcAccount({ + cfg: asConfig({ + channels: { + irc: { + host: "irc.example.com", + nick: "claw", + }, + }, + }), + }); + + expect(account.config.channels).toEqual(["alpha", "beta", "gamma", "delta"]); + } finally { + if (previousChannels === undefined) { + delete process.env.IRC_CHANNELS; + } else { + process.env.IRC_CHANNELS = previousChannels; + } + } + }); + it.runIf(process.platform !== "win32")("rejects symlinked password files", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-irc-account-")); const passwordFile = path.join(dir, "password.txt"); diff --git a/extensions/irc/src/accounts.ts b/extensions/irc/src/accounts.ts index 13d48fffdb7..9367a7d2123 100644 --- a/extensions/irc/src/accounts.ts +++ b/extensions/irc/src/accounts.ts @@ -3,6 +3,7 @@ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core"; import { createAccountListHelpers, normalizeResolvedSecretInputString, + parseOptionalDelimitedEntries, } from "openclaw/plugin-sdk/irc"; import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js"; @@ -42,17 +43,6 @@ function parseIntEnv(value?: string): number | undefined { return parsed; } -function parseListEnv(value?: string): string[] | undefined { - if (!value?.trim()) { - return undefined; - } - const parsed = value - .split(/[\n,;]+/g) - .map((entry) => entry.trim()) - .filter(Boolean); - return parsed.length > 0 ? parsed : undefined; -} - const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefaultIrcAccountId } = createAccountListHelpers("irc", { normalizeAccountId }); export { listIrcAccountIds, resolveDefaultIrcAccountId }; @@ -174,7 +164,9 @@ export function resolveIrcAccount(params: { accountId === DEFAULT_ACCOUNT_ID ? parseIntEnv(process.env.IRC_PORT) : undefined; const port = merged.port ?? envPort ?? (tls ? 6697 : 6667); const envChannels = - accountId === DEFAULT_ACCOUNT_ID ? parseListEnv(process.env.IRC_CHANNELS) : undefined; + accountId === DEFAULT_ACCOUNT_ID + ? parseOptionalDelimitedEntries(process.env.IRC_CHANNELS) + : undefined; const host = ( merged.host?.trim() || diff --git a/src/channels/plugins/helpers.test.ts b/src/channels/plugins/helpers.test.ts index 2b85d7fea06..6b5f56c2ca3 100644 --- a/src/channels/plugins/helpers.test.ts +++ b/src/channels/plugins/helpers.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; -import { buildAccountScopedDmSecurityPolicy, formatPairingApproveHint } from "./helpers.js"; +import { + buildAccountScopedDmSecurityPolicy, + formatPairingApproveHint, + parseOptionalDelimitedEntries, +} from "./helpers.js"; function cfgWithChannel(channelKey: string, accounts?: Record): OpenClawConfig { return { @@ -93,3 +97,18 @@ describe("buildAccountScopedDmSecurityPolicy", () => { }); }); }); + +describe("parseOptionalDelimitedEntries", () => { + it("returns undefined for empty input", () => { + expect(parseOptionalDelimitedEntries(" ")).toBeUndefined(); + }); + + it("splits comma, newline, and semicolon separated entries", () => { + expect(parseOptionalDelimitedEntries("alpha, beta\ngamma; delta")).toEqual([ + "alpha", + "beta", + "gamma", + "delta", + ]); + }); +}); diff --git a/src/channels/plugins/helpers.ts b/src/channels/plugins/helpers.ts index 135547d6e9a..40b01beb4d8 100644 --- a/src/channels/plugins/helpers.ts +++ b/src/channels/plugins/helpers.ts @@ -20,6 +20,17 @@ export function formatPairingApproveHint(channelId: string): string { return `Approve via: ${listCmd} / ${approveCmd}`; } +export function parseOptionalDelimitedEntries(value?: string): string[] | undefined { + if (!value?.trim()) { + return undefined; + } + const parsed = value + .split(/[\n,;]+/g) + .map((entry) => entry.trim()) + .filter(Boolean); + return parsed.length > 0 ? parsed : undefined; +} + export function buildAccountScopedDmSecurityPolicy(params: { cfg: OpenClawConfig; channelKey: string; diff --git a/src/commands/channels/add.ts b/src/commands/channels/add.ts index 882e7f16ca5..ebf80e6a735 100644 --- a/src/commands/channels/add.ts +++ b/src/commands/channels/add.ts @@ -1,5 +1,6 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { listChannelPluginCatalogEntries } from "../../channels/plugins/catalog.js"; +import { parseOptionalDelimitedEntries } from "../../channels/plugins/helpers.js"; import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; import { moveSingleAccountChannelSectionToDefaultAccount } from "../../channels/plugins/setup-helpers.js"; import type { ChannelId, ChannelSetupInput } from "../../channels/plugins/types.js"; @@ -28,17 +29,6 @@ export type ChannelsAddOptions = { dmAllowlist?: string; } & Omit; -function parseList(value: string | undefined): string[] | undefined { - if (!value?.trim()) { - return undefined; - } - const parsed = value - .split(/[\n,;]+/g) - .map((entry) => entry.trim()) - .filter(Boolean); - return parsed.length > 0 ? parsed : undefined; -} - function resolveCatalogChannelEntry(raw: string, cfg: OpenClawConfig | null) { const trimmed = raw.trim().toLowerCase(); if (!trimmed) { @@ -225,8 +215,8 @@ export async function channelsAddCommand( : typeof opts.initialSyncLimit === "string" && opts.initialSyncLimit.trim() ? Number.parseInt(opts.initialSyncLimit, 10) : undefined; - const groupChannels = parseList(opts.groupChannels); - const dmAllowlist = parseList(opts.dmAllowlist); + const groupChannels = parseOptionalDelimitedEntries(opts.groupChannels); + const dmAllowlist = parseOptionalDelimitedEntries(opts.dmAllowlist); const input: ChannelSetupInput = { name: opts.name, diff --git a/src/plugin-sdk/irc.ts b/src/plugin-sdk/irc.ts index 7b2e6d07c8a..2ef8602421f 100644 --- a/src/plugin-sdk/irc.ts +++ b/src/plugin-sdk/irc.ts @@ -9,7 +9,10 @@ export { } from "../channels/plugins/config-helpers.js"; export { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; -export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; +export { + formatPairingApproveHint, + parseOptionalDelimitedEntries, +} from "../channels/plugins/helpers.js"; export type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy,