UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

269 lines (240 loc) 7.98 kB
import { readFileSync } from "node:fs"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; 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; } 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; } function listConfiguredAccountIds(cfg: CoreConfig): string[] { const accounts = cfg.channels?.irc?.accounts; if (!accounts || typeof accounts !== "object") { return []; } const ids = new Set<string>(); for (const key of Object.keys(accounts)) { if (key.trim()) { ids.add(normalizeAccountId(key)); } } return [...ids]; } function resolveAccountConfig(cfg: CoreConfig, accountId: string): IrcAccountConfig | undefined { const accounts = cfg.channels?.irc?.accounts; if (!accounts || typeof accounts !== "object") { return undefined; } const direct = accounts[accountId] as IrcAccountConfig | undefined; if (direct) { return direct; } const normalized = normalizeAccountId(accountId); const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized); return matchKey ? (accounts[matchKey] as IrcAccountConfig | undefined) : undefined; } function mergeIrcAccountConfig(cfg: CoreConfig, accountId: string): IrcAccountConfig { const { accounts: _ignored, ...base } = (cfg.channels?.irc ?? {}) as IrcAccountConfig & { accounts?: unknown; }; const account = resolveAccountConfig(cfg, accountId) ?? {}; const merged: IrcAccountConfig = { ...base, ...account }; if (base.nickserv || account.nickserv) { merged.nickserv = { ...base.nickserv, ...account.nickserv, }; } return merged; } 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()) { try { const filePassword = readFileSync(merged.passwordFile.trim(), "utf-8").trim(); if (filePassword) { return { password: filePassword, source: "passwordFile" as const }; } } catch { // Ignore unreadable files here; status will still surface missing configuration. } } const configPassword = merged.password?.trim(); 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 = base.password?.trim() || envPassword || ""; if (!resolvedPassword && passwordFile) { try { resolvedPassword = readFileSync(passwordFile, "utf-8").trim(); } catch { // Ignore unreadable files; monitor/probe status will surface failures. } } 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 listIrcAccountIds(cfg: CoreConfig): string[] { const ids = listConfiguredAccountIds(cfg); if (ids.length === 0) { return [DEFAULT_ACCOUNT_ID]; } return ids.toSorted((a, b) => a.localeCompare(b)); } export function resolveDefaultIrcAccountId(cfg: CoreConfig): string { const ids = listIrcAccountIds(cfg); if (ids.includes(DEFAULT_ACCOUNT_ID)) { return DEFAULT_ACCOUNT_ID; } return ids[0] ?? DEFAULT_ACCOUNT_ID; } 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 ? parseListEnv(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); }