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