from __future__ import annotations import os from dataclasses import dataclass, field from typing import Any from yuxi.utils.logging_config import logger @dataclass class IRCSetupWizardResult: config: dict[str, Any] = field(default_factory=dict) warnings: list[str] = field(default_factory=list) errors: list[str] = field(default_factory=list) success: bool = False WIZARD_STEPS = [ "prepare", "server", "nickname", "channels", "groupAccess", "authentication", ] _ENV_MAP: dict[str, str] = { "server": "IRC_HOST", "port": "IRC_PORT", "use_tls": "IRC_TLS", "nick": "IRC_NICK", "username": "IRC_USERNAME", "realname": "IRC_REALNAME", "password": "IRC_PASSWORD", "sasl_username": "IRC_SASL_USERNAME", "sasl_password": "IRC_SASL_PASSWORD", "nickserv_password": "IRC_NICKSERV_PASSWORD", "nickserv_service": "IRC_NICKSERV_SERVICE", "nickserv_register_email": "IRC_NICKSERV_REGISTER_EMAIL", "nickserv_password_file": "IRC_NICKSERV_PASSWORD_FILE", "server_password": "IRC_SERVER_PASSWORD", "server_password_file": "IRC_SERVER_PASSWORD_FILE", "dm_policy": "IRC_DM_POLICY", "group_policy": "IRC_GROUP_POLICY", "channels": "IRC_CHANNELS", } def prepare_setup_wizard(existing_config: dict[str, Any] | None = None) -> dict[str, Any]: """Detect environment-variable-based pre-configuration. Scans OS environment variables for IRC-related settings and returns a pre-filled answers dict that can be used as defaults in the wizard. """ answers: dict[str, Any] = dict(existing_config) if existing_config else {} for key, env_var in _ENV_MAP.items(): env_val = os.environ.get(env_var, "") if not env_val: continue if key in ("port",): try: answers[key] = int(env_val) except ValueError: pass elif key in ("use_tls",): answers[key] = env_val.lower() in ("1", "true", "yes") else: answers[key] = env_val if "IRC_CHANNELS" in os.environ and "channels" not in answers: answers["channels"] = os.environ["IRC_CHANNELS"] if answers: logger.info(f"IRC setup wizard: detected {len(answers)} env-var preconfigs") return answers def run_setup_wizard( answers: dict[str, Any], existing_config: dict[str, Any] | None = None, ) -> IRCSetupWizardResult: result = IRCSetupWizardResult() config = dict(existing_config) if existing_config else {} server = answers.get("server", "") port = answers.get("port", 6697) use_tls = answers.get("use_tls", True) if not server: result.errors.append("Server hostname is required") return result if not isinstance(port, int) or port < 1 or port > 65535: result.errors.append("Port must be between 1 and 65535") return result config["server"] = server config["port"] = port config["use_tls"] = use_tls if not use_tls: result.warnings.append("TLS is disabled — connection will not be encrypted") nick = answers.get("nick", "") if nick: config["nick"] = nick config["username"] = answers.get("username", nick) config["realname"] = answers.get("realname", f"ForcePilot IRC Bot ({nick})") else: result.errors.append("Nickname is required") return result channels = answers.get("channels", "") if channels: if isinstance(channels, str): config["auto_join_channels"] = [ch.strip() for ch in channels.split(",") if ch.strip()] elif isinstance(channels, list): config["auto_join_channels"] = channels dm_policy = answers.get("dm_policy", "pairing") group_policy = answers.get("group_policy", "allowlist") group_access = answers.get("groupAccess", "") if group_access and isinstance(group_access, str): entries = [e.strip() for e in group_access.split(",") if e.strip()] if entries and not config.get("groupAllowFrom"): config["groupAllowFrom"] = entries allow_from = answers.get("allowFrom", "") if allow_from and isinstance(allow_from, str): entries = [e.strip() for e in allow_from.split(",") if e.strip()] if entries and not config.get("allowFrom"): config["allowFrom"] = entries config["dm_policy"] = dm_policy config["group_policy"] = group_policy if dm_policy == "open": result.warnings.append("DM policy is 'open' — anyone can trigger the bot via DM") if group_policy == "open": result.warnings.append("Group policy is 'open' — anyone can trigger the bot in channels") password = answers.get("password", "") use_sasl = answers.get("use_sasl", False) sasl_username = answers.get("sasl_username", "") sasl_password = answers.get("sasl_password", "") nickserv_password = answers.get("nickserv_password", "") if password: config["password"] = password if use_sasl: if not sasl_username: sasl_username = nick if not sasl_password: result.errors.append("SASL password is required when SASL is enabled") return result config["use_sasl"] = True config["sasl_username"] = sasl_username config["sasl_password"] = sasl_password config["auth_mode"] = "sasl" elif nickserv_password: config["nickserv_enabled"] = True config["nickserv_password"] = nickserv_password config["auth_mode"] = "nickserv" else: logger.info("IRC setup: no authentication configured") result.config = config result.success = True _add_cross_field_validation(config, result) if not result.errors: result.success = True else: result.success = False return result def validate_config(config: dict[str, Any]) -> IRCSetupWizardResult: result = IRCSetupWizardResult() if not config.get("server"): result.errors.append("Server hostname is required") if not config.get("nick"): result.errors.append("Nickname is required") port = config.get("port", 6697) if not isinstance(port, int) or port < 1 or port > 65535: result.errors.append(f"Invalid port: {port}") use_sasl = config.get("use_sasl", False) if use_sasl: if not config.get("sasl_username"): result.errors.append("SASL username is required when SASL is enabled") if not config.get("sasl_password"): result.errors.append("SASL password is required when SASL is enabled") use_tls = config.get("use_tls", True) if not use_tls: result.warnings.append("TLS is disabled") channels = config.get("auto_join_channels", []) if not channels: result.warnings.append("No channels configured — bot will not auto-join any channels") if not result.errors: result.success = True result.config = config _add_cross_field_validation(config, result) if result.errors: result.success = False return result def _add_cross_field_validation(config: dict[str, Any], result: IRCSetupWizardResult) -> None: nickserv_register = config.get("nickserv_register", False) if nickserv_register: email = config.get("nickserv_register_email", "") if not email: result.errors.append("nickserv_register: NickServ REGISTER requires nickserv_register_email") dm_policy = config.get("dm_policy", "pairing") if dm_policy == "open": allow_from = config.get("allowFrom", []) if isinstance(allow_from, list) and "*" not in allow_from: result.errors.append("dm_policy: dm_policy=open requires allowFrom to include '*'")