新增IRC协议相关的全套工具模块,包括: - 核心协议解析与CTCP处理 - 消息发送缓存与文本 sanitize - 账号配置管理与运行时状态 - 命令处理与权限控制 - 服务发现与诊断工具 - 多账号网关与配置加载
234 lines
7.6 KiB
Python
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 '*'")
|