import logging import os from pathlib import Path from yuxi.channel.extensions.irc.config import IrcAccountConfig, IrcConfig, IrcNickServConfig from yuxi.channel.extensions.irc.types import ResolvedIrcAccount logger = logging.getLogger(__name__) def _read_password_file(file_path: str) -> str: p = Path(file_path) if not p.is_file(): logger.warning("Password file not found: %s", file_path) return "" if p.is_symlink(): logger.warning("Password file is a symlink, refusing to read: %s", file_path) return "" try: content = p.read_text(encoding="utf-8").strip() return content except Exception as e: logger.warning("Failed to read password file %s: %s", file_path, e) return "" def _resolve_password(password: str, password_file: str | None, account_id: str) -> tuple[str, str]: if account_id == "default": env_val = os.environ.get("IRC_PASSWORD") if env_val: return env_val, "env" if password_file: content = _read_password_file(password_file) if content: return content, "passwordFile" if password: return password, "config" return "", "none" def _resolve_nickserv_password(nickserv: IrcNickServConfig, account_id: str) -> tuple[str, str]: if nickserv.password: return nickserv.password, "config" if account_id == "default": env_val = os.environ.get("IRC_NICKSERV_PASSWORD") if env_val: return env_val, "env" if nickserv.password_file: content = _read_password_file(nickserv.password_file) if content: return content, "passwordFile" return "", "none" def _merge_channels(global_channels: list[str], account_channels: list[str]) -> list[str]: if account_channels: return account_channels return global_channels def _merge_groups( global_groups: dict[str, object], account_groups: dict[str, object], ) -> dict[str, object]: merged = dict(global_groups) merged.update(account_groups) return merged def resolve_irc_account(cfg: IrcConfig, account_id: str = "default") -> ResolvedIrcAccount: account_cfg = cfg.accounts.get(account_id) if account_cfg is None: if account_id == "default": account_cfg = IrcAccountConfig( host=cfg.host, port=cfg.port, tls=cfg.tls, nick=cfg.nick, username=cfg.username, realname=cfg.realname, password=cfg.password, password_file=cfg.password_file, nickserv=cfg.nickserv, dm_policy=cfg.dm_policy, allow_from=cfg.allow_from, group_policy=cfg.group_policy, group_allow_from=cfg.group_allow_from, groups=cfg.groups, channels=cfg.channels, dangerously_allow_name_matching=cfg.dangerously_allow_name_matching, text_chunk_limit=cfg.text_chunk_limit, ) else: raise KeyError(f"Account '{account_id}' not found") host = account_cfg.host or cfg.host port = account_cfg.port or cfg.port tls = account_cfg.tls if account_cfg.host else cfg.tls nick = account_cfg.nick or cfg.nick username = account_cfg.username or cfg.username realname = account_cfg.realname or cfg.realname password, password_source = _resolve_password( account_cfg.password or cfg.password, account_cfg.password_file or cfg.password_file, account_id, ) nickserv_cfg = account_cfg.nickserv if not nickserv_cfg.password and not nickserv_cfg.password_file: nickserv_cfg = cfg.nickserv nickserv_password, nickserv_password_source = _resolve_nickserv_password( nickserv_cfg, account_id ) channels = _merge_channels(cfg.channels, account_cfg.channels) groups = _merge_groups(cfg.groups, account_cfg.groups) dm_policy = account_cfg.dm_policy if account_cfg.host else cfg.dm_policy allow_from = account_cfg.allow_from or cfg.allow_from group_policy = account_cfg.group_policy if account_cfg.host else cfg.group_policy group_allow_from = account_cfg.group_allow_from or cfg.group_allow_from dangerously_allow_name_matching = ( account_cfg.dangerously_allow_name_matching or cfg.dangerously_allow_name_matching ) resolved_config = IrcAccountConfig( name=account_cfg.name or account_id, enabled=account_cfg.enabled, dangerously_allow_name_matching=dangerously_allow_name_matching, host=host, port=port, tls=tls, nick=nick, username=username, realname=realname, password=password, password_file=account_cfg.password_file or cfg.password_file, nickserv=nickserv_cfg, dm_policy=dm_policy, allow_from=allow_from, group_policy=group_policy, group_allow_from=group_allow_from, groups=groups, channels=channels, text_chunk_limit=account_cfg.text_chunk_limit, block_streaming=account_cfg.block_streaming, ) configured = bool(host and nick) if not configured and account_id == "default": env_host = os.environ.get("IRC_HOST") env_nick = os.environ.get("IRC_NICK") if env_host and env_nick: configured = True return ResolvedIrcAccount( account_id=account_id, enabled=account_cfg.enabled, name=resolved_config.name, configured=configured, host=host, port=port, tls=tls, nick=nick, username=username, realname=realname, password=password, password_source=password_source, nickserv_password=nickserv_password, nickserv_password_source=nickserv_password_source, config=resolved_config, ) def list_irc_account_ids(cfg: IrcConfig) -> list[str]: if cfg.accounts: return list(cfg.accounts.keys()) return ["default"] def has_configured_state(cfg: IrcConfig) -> bool: if cfg.host and cfg.nick: return True for acc in cfg.accounts.values(): if acc.host and acc.nick: return True if os.environ.get("IRC_HOST") and os.environ.get("IRC_NICK"): return True return False