ForcePilot/backend/package/yuxi/channels/adapters/irc/setup.py
Kris 2dd1a075f2 feat(irc): 实现完整的IRC适配器基础功能模块
新增IRC协议相关的全套工具模块,包括:
- 核心协议解析与CTCP处理
- 消息发送缓存与文本 sanitize
- 账号配置管理与运行时状态
- 命令处理与权限控制
- 服务发现与诊断工具
- 多账号网关与配置加载
2026-05-12 00:45:10 +08:00

234 lines
7.6 KiB
Python

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 '*'")