From 9785b44307fa3f96ccae15e5726fd38a592af1e4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 19:12:38 -0700 Subject: [PATCH] IRC: split setup adapter helpers --- extensions/irc/src/channel.ts | 3 +- extensions/irc/src/setup-core.ts | 147 +++++++++++++++++++++++++ extensions/irc/src/setup-surface.ts | 162 ++++------------------------ 3 files changed, 169 insertions(+), 143 deletions(-) create mode 100644 extensions/irc/src/setup-core.ts diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index b1fd0fc89d8..ca53d53a93d 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -36,7 +36,8 @@ import { resolveIrcGroupMatch, resolveIrcRequireMention } from "./policy.js"; import { probeIrc } from "./probe.js"; import { getIrcRuntime } from "./runtime.js"; import { sendMessageIrc } from "./send.js"; -import { ircSetupAdapter, ircSetupWizard } from "./setup-surface.js"; +import { ircSetupAdapter } from "./setup-core.js"; +import { ircSetupWizard } from "./setup-surface.js"; import type { CoreConfig, IrcProbe } from "./types.js"; const meta = getChatChannelMeta("irc"); diff --git a/extensions/irc/src/setup-core.ts b/extensions/irc/src/setup-core.ts new file mode 100644 index 00000000000..45f9041f973 --- /dev/null +++ b/extensions/irc/src/setup-core.ts @@ -0,0 +1,147 @@ +import { + setTopLevelChannelAllowFrom, + setTopLevelChannelDmPolicyWithAllowFrom, +} from "../../../src/channels/plugins/onboarding/helpers.js"; +import { + applyAccountNameToChannelSection, + patchScopedAccountConfig, +} from "../../../src/channels/plugins/setup-helpers.js"; +import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js"; +import type { ChannelSetupInput } from "../../../src/channels/plugins/types.core.js"; +import type { DmPolicy } from "../../../src/config/types.js"; +import { normalizeAccountId } from "../../../src/routing/session-key.js"; +import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js"; + +const channel = "irc" as const; + +type IrcSetupInput = ChannelSetupInput & { + host?: string; + port?: number | string; + tls?: boolean; + nick?: string; + username?: string; + realname?: string; + channels?: string[]; + password?: string; +}; + +export function parsePort(raw: string, fallback: number): number { + const trimmed = raw.trim(); + if (!trimmed) { + return fallback; + } + const parsed = Number.parseInt(trimmed, 10); + if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) { + return fallback; + } + return parsed; +} + +export function updateIrcAccountConfig( + cfg: CoreConfig, + accountId: string, + patch: Partial, +): CoreConfig { + return patchScopedAccountConfig({ + cfg, + channelKey: channel, + accountId, + patch, + ensureChannelEnabled: false, + ensureAccountEnabled: false, + }) as CoreConfig; +} + +export function setIrcDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig { + return setTopLevelChannelDmPolicyWithAllowFrom({ + cfg, + channel, + dmPolicy, + }) as CoreConfig; +} + +export function setIrcAllowFrom(cfg: CoreConfig, allowFrom: string[]): CoreConfig { + return setTopLevelChannelAllowFrom({ + cfg, + channel, + allowFrom, + }) as CoreConfig; +} + +export function setIrcNickServ( + cfg: CoreConfig, + accountId: string, + nickserv?: IrcNickServConfig, +): CoreConfig { + return updateIrcAccountConfig(cfg, accountId, { nickserv }); +} + +export function setIrcGroupAccess( + cfg: CoreConfig, + accountId: string, + policy: "open" | "allowlist" | "disabled", + entries: string[], + normalizeGroupEntry: (raw: string) => string | null, +): CoreConfig { + if (policy !== "allowlist") { + return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: policy }); + } + const normalizedEntries = [ + ...new Set(entries.map((entry) => normalizeGroupEntry(entry)).filter(Boolean)), + ]; + const groups = Object.fromEntries(normalizedEntries.map((entry) => [entry, {}])); + return updateIrcAccountConfig(cfg, accountId, { + enabled: true, + groupPolicy: "allowlist", + groups, + }); +} + +export const ircSetupAdapter: ChannelSetupAdapter = { + resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), + applyAccountName: ({ cfg, accountId, name }) => + applyAccountNameToChannelSection({ + cfg, + channelKey: channel, + accountId, + name, + }), + validateInput: ({ input }) => { + const setupInput = input as IrcSetupInput; + if (!setupInput.host?.trim()) { + return "IRC requires host."; + } + if (!setupInput.nick?.trim()) { + return "IRC requires nick."; + } + return null; + }, + applyAccountConfig: ({ cfg, accountId, input }) => { + const setupInput = input as IrcSetupInput; + const namedConfig = applyAccountNameToChannelSection({ + cfg, + channelKey: channel, + accountId, + name: setupInput.name, + }); + const portInput = + typeof setupInput.port === "number" ? String(setupInput.port) : String(setupInput.port ?? ""); + const patch: Partial = { + enabled: true, + host: setupInput.host?.trim(), + port: portInput ? parsePort(portInput, setupInput.tls === false ? 6667 : 6697) : undefined, + tls: setupInput.tls, + nick: setupInput.nick?.trim(), + username: setupInput.username?.trim(), + realname: setupInput.realname?.trim(), + password: setupInput.password?.trim(), + channels: setupInput.channels, + }; + return patchScopedAccountConfig({ + cfg: namedConfig, + channelKey: channel, + accountId, + patch, + }) as CoreConfig; + }, +}; diff --git a/extensions/irc/src/setup-surface.ts b/extensions/irc/src/setup-surface.ts index aaee61a9532..63a7bec920b 100644 --- a/extensions/irc/src/setup-surface.ts +++ b/extensions/irc/src/setup-surface.ts @@ -2,18 +2,10 @@ import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/on import { resolveOnboardingAccountId, setOnboardingChannelEnabled, - setTopLevelChannelAllowFrom, - setTopLevelChannelDmPolicyWithAllowFrom, } from "../../../src/channels/plugins/onboarding/helpers.js"; -import { - applyAccountNameToChannelSection, - patchScopedAccountConfig, -} from "../../../src/channels/plugins/setup-helpers.js"; import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; -import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js"; -import type { ChannelSetupInput } from "../../../src/channels/plugins/types.core.js"; import type { DmPolicy } from "../../../src/config/types.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js"; +import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js"; import { formatDocsLink } from "../../../src/terminal/links.js"; import type { WizardPrompter } from "../../../src/wizard/prompts.js"; import { listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount } from "./accounts.js"; @@ -22,23 +14,21 @@ import { normalizeIrcAllowEntry, normalizeIrcMessagingTarget, } from "./normalize.js"; +import { + ircSetupAdapter, + parsePort, + setIrcAllowFrom, + setIrcDmPolicy, + setIrcGroupAccess, + setIrcNickServ, + updateIrcAccountConfig, +} from "./setup-core.js"; import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js"; const channel = "irc" as const; const USE_ENV_FLAG = "__ircUseEnv"; const TLS_FLAG = "__ircTls"; -type IrcSetupInput = ChannelSetupInput & { - host?: string; - port?: number | string; - tls?: boolean; - nick?: string; - username?: string; - realname?: string; - channels?: string[]; - password?: string; -}; - function parseListInput(raw: string): string[] { return raw .split(/[\n,;]+/g) @@ -46,18 +36,6 @@ function parseListInput(raw: string): string[] { .filter(Boolean); } -function parsePort(raw: string, fallback: number): number { - const trimmed = raw.trim(); - if (!trimmed) { - return fallback; - } - const parsed = Number.parseInt(trimmed, 10); - if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) { - return fallback; - } - return parsed; -} - function normalizeGroupEntry(raw: string): string | null { const trimmed = raw.trim(); if (!trimmed) { @@ -73,65 +51,6 @@ function normalizeGroupEntry(raw: string): string | null { return `#${normalized.replace(/^#+/, "")}`; } -function updateIrcAccountConfig( - cfg: CoreConfig, - accountId: string, - patch: Partial, -): CoreConfig { - return patchScopedAccountConfig({ - cfg, - channelKey: channel, - accountId, - patch, - ensureChannelEnabled: false, - ensureAccountEnabled: false, - }) as CoreConfig; -} - -function setIrcDmPolicy(cfg: CoreConfig, dmPolicy: DmPolicy): CoreConfig { - return setTopLevelChannelDmPolicyWithAllowFrom({ - cfg, - channel, - dmPolicy, - }) as CoreConfig; -} - -function setIrcAllowFrom(cfg: CoreConfig, allowFrom: string[]): CoreConfig { - return setTopLevelChannelAllowFrom({ - cfg, - channel, - allowFrom, - }) as CoreConfig; -} - -function setIrcNickServ( - cfg: CoreConfig, - accountId: string, - nickserv?: IrcNickServConfig, -): CoreConfig { - return updateIrcAccountConfig(cfg, accountId, { nickserv }); -} - -function setIrcGroupAccess( - cfg: CoreConfig, - accountId: string, - policy: "open" | "allowlist" | "disabled", - entries: string[], -): CoreConfig { - if (policy !== "allowlist") { - return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: policy }); - } - const normalizedEntries = [ - ...new Set(entries.map((entry) => normalizeGroupEntry(entry)).filter(Boolean)), - ]; - const groups = Object.fromEntries(normalizedEntries.map((entry) => [entry, {}])); - return updateIrcAccountConfig(cfg, accountId, { - enabled: true, - groupPolicy: "allowlist", - groups, - }); -} - async function promptIrcAllowFrom(params: { cfg: CoreConfig; prompter: WizardPrompter; @@ -264,55 +183,6 @@ const ircDmPolicy: ChannelOnboardingDmPolicy = { }), }; -export const ircSetupAdapter: ChannelSetupAdapter = { - resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), - applyAccountName: ({ cfg, accountId, name }) => - applyAccountNameToChannelSection({ - cfg, - channelKey: channel, - accountId, - name, - }), - validateInput: ({ input }) => { - const setupInput = input as IrcSetupInput; - if (!setupInput.host?.trim()) { - return "IRC requires host."; - } - if (!setupInput.nick?.trim()) { - return "IRC requires nick."; - } - return null; - }, - applyAccountConfig: ({ cfg, accountId, input }) => { - const setupInput = input as IrcSetupInput; - const namedConfig = applyAccountNameToChannelSection({ - cfg, - channelKey: channel, - accountId, - name: setupInput.name, - }); - const portInput = - typeof setupInput.port === "number" ? String(setupInput.port) : String(setupInput.port ?? ""); - const patch: Partial = { - enabled: true, - host: setupInput.host?.trim(), - port: portInput ? parsePort(portInput, setupInput.tls === false ? 6667 : 6697) : undefined, - tls: setupInput.tls, - nick: setupInput.nick?.trim(), - username: setupInput.username?.trim(), - realname: setupInput.realname?.trim(), - password: setupInput.password?.trim(), - channels: setupInput.channels, - }; - return patchScopedAccountConfig({ - cfg: namedConfig, - channelKey: channel, - accountId, - patch, - }) as CoreConfig; - }, -}; - export const ircSetupWizard: ChannelSetupWizard = { channel, status: { @@ -509,11 +379,17 @@ export const ircSetupWizard: ChannelSetupWizard = { updatePrompt: ({ cfg, accountId }) => Boolean(resolveIrcAccount({ cfg: cfg as CoreConfig, accountId }).config.groups), setPolicy: ({ cfg, accountId, policy }) => - setIrcGroupAccess(cfg as CoreConfig, accountId, policy, []), + setIrcGroupAccess(cfg as CoreConfig, accountId, policy, [], normalizeGroupEntry), resolveAllowlist: async ({ entries }) => [...new Set(entries.map((entry) => normalizeGroupEntry(entry)).filter(Boolean))] as string[], applyAllowlist: ({ cfg, accountId, resolved }) => - setIrcGroupAccess(cfg as CoreConfig, accountId, "allowlist", resolved as string[]), + setIrcGroupAccess( + cfg as CoreConfig, + accountId, + "allowlist", + resolved as string[], + normalizeGroupEntry, + ), }, allowFrom: { helpTitle: "IRC allowlist", @@ -584,3 +460,5 @@ export const ircSetupWizard: ChannelSetupWizard = { dmPolicy: ircDmPolicy, disable: (cfg) => setOnboardingChannelEnabled(cfg, channel, false), }; + +export { ircSetupAdapter };