Files
openclaw/extensions/irc/src/accounts.ts
2026-03-10 23:40:10 +00:00

258 lines
7.5 KiB
TypeScript

import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/core";
import {
createAccountListHelpers,
normalizeResolvedSecretInputString,
} from "openclaw/plugin-sdk/irc";
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;
}
const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefaultIrcAccountId } =
createAccountListHelpers("irc", { normalizeAccountId });
export { listIrcAccountIds, resolveDefaultIrcAccountId };
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,
defaultAccount: _ignoredDefaultAccount,
...base
} = (cfg.channels?.irc ?? {}) as IrcAccountConfig & {
accounts?: unknown;
defaultAccount?: 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()) {
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 ? 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);
}