ForcePilot/backend/package/yuxi/channel/extensions/irc/accounts.py
Kris 061cc7076b feat(channel): 添加 IRC 渠道扩展插件
新增 IRC 渠道的完整扩展实现,包括客户端连接管理、消息收发与流式处理、安全策略与配对验证、消息去重与规范化、状态监控与健康探测、配置模型与账户管理、错误处理等模块。
2026-05-21 10:59:12 +08:00

190 lines
6.2 KiB
Python

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