IRC: split setup adapter helpers

This commit is contained in:
Vincent Koc
2026-03-15 19:12:38 -07:00
parent 10f4a03de8
commit 9785b44307
3 changed files with 169 additions and 143 deletions

View File

@@ -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");

View File

@@ -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<IrcAccountConfig>,
): 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<IrcAccountConfig> = {
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;
},
};

View File

@@ -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<IrcAccountConfig>,
): 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<IrcAccountConfig> = {
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 };