新增IRC协议相关的全套工具模块,包括: - 核心协议解析与CTCP处理 - 消息发送缓存与文本 sanitize - 账号配置管理与运行时状态 - 命令处理与权限控制 - 服务发现与诊断工具 - 多账号网关与配置加载
139 lines
3.8 KiB
Python
139 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
from .normalize import build_allowlist_candidates, resolve_allowlist_match
|
|
from .protocol import parse_irc_prefix
|
|
|
|
_COMMAND_PREFIX_RE = re.compile(r"^[!.]\w+")
|
|
|
|
|
|
def should_handle_text_commands(text: str, config: dict[str, Any]) -> bool:
|
|
if not config.get("commands", {}).get("enabled", False):
|
|
return False
|
|
return bool(_COMMAND_PREFIX_RE.match(text.strip()))
|
|
|
|
|
|
def has_control_command(text: str, config: dict[str, Any]) -> bool:
|
|
if not config.get("commands", {}).get("enabled", False):
|
|
return False
|
|
stripped = text.strip()
|
|
return bool(_COMMAND_PREFIX_RE.match(stripped))
|
|
|
|
|
|
def parse_command(text: str) -> tuple[str, list[str]]:
|
|
stripped = text.strip()
|
|
parts = stripped.split(maxsplit=1)
|
|
command = parts[0].lstrip("!.")
|
|
args = parts[1].split() if len(parts) > 1 else []
|
|
return command, args
|
|
|
|
|
|
def check_command_access(
|
|
sender_nick: str,
|
|
command: str,
|
|
config: dict[str, Any],
|
|
access_groups: dict[str, list[str]] | None = None,
|
|
) -> bool:
|
|
use_access_groups = config.get("commands", {}).get("useAccessGroups", False)
|
|
if not use_access_groups:
|
|
return True
|
|
|
|
if access_groups is None:
|
|
access_groups = {}
|
|
|
|
command_acl = _resolve_command_acl(command, config)
|
|
if not command_acl:
|
|
return True
|
|
|
|
for allowed_group in command_acl:
|
|
members = access_groups.get(allowed_group, [])
|
|
if sender_nick in members:
|
|
return True
|
|
|
|
logger.warning(f"IRC command '{command}' access denied for {sender_nick}")
|
|
return False
|
|
|
|
|
|
def _resolve_command_acl(command: str, config: dict[str, Any]) -> list[str]:
|
|
acl = config.get("commands", {}).get("acl", {})
|
|
return acl.get(command, acl.get("*", []))
|
|
|
|
|
|
def sender_allowed_for_commands(
|
|
sender_prefix: str | None,
|
|
sender_nick: str,
|
|
allow_from: list[str],
|
|
) -> bool:
|
|
if not allow_from:
|
|
return False
|
|
prefix_info = parse_irc_prefix(sender_prefix)
|
|
candidates = build_allowlist_candidates(
|
|
sender_nick,
|
|
prefix_info.get("user") or None,
|
|
prefix_info.get("host") or None,
|
|
)
|
|
match = resolve_allowlist_match(candidates, allow_from)
|
|
if match:
|
|
logger.debug(f"IRC command allowlist match for {sender_nick}: {match}")
|
|
return True
|
|
logger.debug(f"IRC command blocked for {sender_nick}, candidates: {candidates}")
|
|
return False
|
|
|
|
|
|
def _resolve_command_response(command: str, args: list[str], config: dict[str, Any]) -> str | None:
|
|
custom_commands = config.get("commands", {}).get("custom", {})
|
|
cmd_def = custom_commands.get(command)
|
|
if isinstance(cmd_def, str):
|
|
return cmd_def
|
|
if isinstance(cmd_def, dict):
|
|
return cmd_def.get("response", "")
|
|
return None
|
|
|
|
|
|
SUPPORTED_COMMANDS = frozenset(
|
|
{
|
|
"help",
|
|
"ping",
|
|
"status",
|
|
"channels",
|
|
"about",
|
|
}
|
|
)
|
|
|
|
|
|
def _format_help_text(config: dict[str, Any]) -> str:
|
|
lines = ["Available commands:"]
|
|
custom_commands = config.get("commands", {}).get("custom", {})
|
|
all_commands = set(SUPPORTED_COMMANDS) | set(custom_commands.keys())
|
|
for cmd in sorted(all_commands):
|
|
lines.append(f" !{cmd}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
async def handle_command(
|
|
command: str,
|
|
args: list[str],
|
|
sender_nick: str,
|
|
target: str,
|
|
config: dict[str, Any],
|
|
send_fn,
|
|
) -> str | None:
|
|
if command == "help":
|
|
return _format_help_text(config)
|
|
if command == "ping":
|
|
return "Pong!"
|
|
if command == "status":
|
|
return "IRC adapter is running"
|
|
if command == "about":
|
|
return "ForcePilot IRC Bot"
|
|
|
|
custom_response = _resolve_command_response(command, args, config)
|
|
if custom_response:
|
|
return custom_response
|
|
|
|
return None
|