import { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution"; import { parseOptionalDelimitedEntries } from "openclaw/plugin-sdk/core"; import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core"; import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input"; import type { CoreConfig, IrcAccountConfig, IrcNickServConfig } from "./types.js"; const TRUTHY_ENV = new Set(["true", "1", "yes", "on"]); export type ResolvedIrcAccount = { accountId: string; enabled: boolean; name?: string; configured: boolean; host: string; port: number; tls: boolean; nick: string; username: string; realname: string; password: string; passwordSource: "env" | "passwordFile" | "config" | "none"; config: IrcAccountConfig; }; function parseTruthy(value?: string): boolean { if (!value) { return false; } return TRUTHY_ENV.has(value.trim().toLowerCase()); } function parseIntEnv(value?: string): number | undefined { if (!value?.trim()) { return undefined; } const parsed = Number.parseInt(value.trim(), 10); if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535) { return undefined; } return parsed; } const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefaultIrcAccountId } = createAccountListHelpers("irc", { normalizeAccountId }); export { listIrcAccountIds, resolveDefaultIrcAccountId }; function mergeIrcAccountConfig(cfg: CoreConfig, accountId: string): IrcAccountConfig { return resolveMergedAccountConfig({ channelConfig: cfg.channels?.irc as IrcAccountConfig | undefined, accounts: cfg.channels?.irc?.accounts as Record> | undefined, accountId, omitKeys: ["defaultAccount"], normalizeAccountId, nestedObjectKeys: ["nickserv"], }); } function resolvePassword(accountId: string, merged: IrcAccountConfig) { if (accountId === DEFAULT_ACCOUNT_ID) { const envPassword = process.env.IRC_PASSWORD?.trim(); if (envPassword) { return { password: envPassword, source: "env" as const }; } } if (merged.passwordFile?.trim()) { const filePassword = tryReadSecretFileSync(merged.passwordFile, "IRC password file", { rejectSymlink: true, }); if (filePassword) { return { password: filePassword, source: "passwordFile" as const }; } } const configPassword = normalizeResolvedSecretInputString({ value: merged.password, path: `channels.irc.accounts.${accountId}.password`, }); if (configPassword) { return { password: configPassword, source: "config" as const }; } return { password: "", source: "none" as const }; } function resolveNickServConfig(accountId: string, nickserv?: IrcNickServConfig): IrcNickServConfig { const base = nickserv ?? {}; const envPassword = accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_PASSWORD?.trim() : undefined; const envRegisterEmail = accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_REGISTER_EMAIL?.trim() : undefined; const passwordFile = base.passwordFile?.trim(); let resolvedPassword = normalizeResolvedSecretInputString({ value: base.password, path: `channels.irc.accounts.${accountId}.nickserv.password`, }) || envPassword || ""; if (!resolvedPassword && passwordFile) { resolvedPassword = tryReadSecretFileSync(passwordFile, "IRC NickServ password file", { rejectSymlink: true, }) ?? ""; } const merged: IrcNickServConfig = { ...base, service: base.service?.trim() || undefined, passwordFile: passwordFile || undefined, password: resolvedPassword || undefined, registerEmail: base.registerEmail?.trim() || envRegisterEmail || undefined, }; return merged; } export function resolveIrcAccount(params: { cfg: CoreConfig; accountId?: string | null; }): ResolvedIrcAccount { const hasExplicitAccountId = Boolean(params.accountId?.trim()); const baseEnabled = params.cfg.channels?.irc?.enabled !== false; const resolve = (accountId: string) => { const merged = mergeIrcAccountConfig(params.cfg, accountId); const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; const tls = typeof merged.tls === "boolean" ? merged.tls : accountId === DEFAULT_ACCOUNT_ID && process.env.IRC_TLS ? parseTruthy(process.env.IRC_TLS) : true; const envPort = accountId === DEFAULT_ACCOUNT_ID ? parseIntEnv(process.env.IRC_PORT) : undefined; const port = merged.port ?? envPort ?? (tls ? 6697 : 6667); const envChannels = accountId === DEFAULT_ACCOUNT_ID ? parseOptionalDelimitedEntries(process.env.IRC_CHANNELS) : undefined; const host = ( merged.host?.trim() || (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_HOST?.trim() : "") || "" ).trim(); const nick = ( merged.nick?.trim() || (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICK?.trim() : "") || "" ).trim(); const username = ( merged.username?.trim() || (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_USERNAME?.trim() : "") || nick || "openclaw" ).trim(); const realname = ( merged.realname?.trim() || (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_REALNAME?.trim() : "") || "OpenClaw" ).trim(); const passwordResolution = resolvePassword(accountId, merged); const nickserv = resolveNickServConfig(accountId, merged.nickserv); const config: IrcAccountConfig = { ...merged, channels: merged.channels ?? envChannels, tls, port, host, nick, username, realname, nickserv, }; return { accountId, enabled, name: merged.name?.trim() || undefined, configured: Boolean(host && nick), host, port, tls, nick, username, realname, password: passwordResolution.password, passwordSource: passwordResolution.source, config, } satisfies ResolvedIrcAccount; }; const normalized = normalizeAccountId(params.accountId); const primary = resolve(normalized); if (hasExplicitAccountId) { return primary; } if (primary.configured) { return primary; } const fallbackId = resolveDefaultIrcAccountId(params.cfg); if (fallbackId === primary.accountId) { return primary; } const fallback = resolve(fallbackId); if (!fallback.configured) { return primary; } return fallback; } export function listEnabledIrcAccounts(cfg: CoreConfig): ResolvedIrcAccount[] { return listIrcAccountIds(cfg) .map((accountId) => resolveIrcAccount({ cfg, accountId })) .filter((account) => account.enabled); }