# modules/docs_site/docs_site.py import json import threading from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse from typing import Any, Dict, List, Optional, Tuple import discord from discord.ext import commands from discord import app_commands from modules.common.settings import cfg import mod_perms # for name/qualname detection only (no calls) # ----------------------------- # Helpers: command introspection # ----------------------------- def _is_perm_check(fn) -> Tuple[bool, List[str]]: """ Detects if a prefix/hybrid command check enforces guild/admin permissions. Returns (has_perm_check, [perm_names]). """ try: name = getattr(fn, "__qualname__", "") or getattr(fn, "__name__", "") except Exception: name = "" # discord.py wraps has_permissions/has_guild_permissions checks; they carry attrs sometimes. perms = [] try: # Some check predicates store desired perms on closure/cell vars; best-effort: code = getattr(fn, "__code__", None) if code and code.co_freevars and getattr(fn, "__closure__", None): for cell in fn.__closure__ or []: val = getattr(cell, "cell_contents", None) if isinstance(val, dict): # likely {'manage_messages': True, ...} for k, v in val.items(): if v: perms.append(str(k)) except Exception: pass has_perm = any(p for p in perms) return has_perm, perms def _looks_like_mod_check(fn) -> bool: """ Heuristic: identify checks referring to your moderator helpers. """ try: qn = getattr(fn, "__qualname__", "") or "" mod_names = ( "is_moderator_member", "is_moderator_userid", "require_mod_ctx", "require_mod_interaction", ) if any(m in qn for m in mod_names): return True # Also check module path/name: mod = getattr(fn, "__module__", "") or "" if "mod_perms" in mod: return True except Exception: pass return False def _is_mod_command_prefix(cmd: commands.Command) -> Tuple[bool, List[str]]: """ For prefix/hybrid commands: inspect .checks for moderator checks or admin perms. """ is_mod = False perms: List[str] = [] try: for chk in getattr(cmd, "checks", []) or []: m = _looks_like_mod_check(chk) p_flag, p_list = _is_perm_check(chk) if m: is_mod = True if p_flag: perms.extend(p_list) except Exception: pass # If command declares directly required perms (rare), capture them too: # No official attribute on commands.Command; we rely on checks only. return is_mod, sorted(set(perms)) def _is_mod_command_slash(cmd: app_commands.Command) -> Tuple[bool, List[str]]: """ For slash commands: inspect .checks (app_commands) if present. """ is_mod = False perms: List[str] = [] try: for chk in getattr(cmd, "checks", []) or []: m = _looks_like_mod_check(chk) # app_commands has no built-in has_permissions; users often wrap their own. if m: is_mod = True except Exception: pass # Best‑effort: look into ._checks (private) if available try: for chk in getattr(cmd, "_checks", []) or []: if _looks_like_mod_check(chk): is_mod = True except Exception: pass return is_mod, perms def _command_usage_prefix(cmd: commands.Command) -> str: # Prefer .usage, else generate "/help"-like usage. if cmd.usage: return cmd.usage try: # Build basic placeholder usage params = [] for k, p in cmd.clean_params.items(): if p.kind.name.lower().startswith("var"): params.append(f"[{k}...]") elif p.default is p.empty: params.append(f"<{k}>") else: params.append(f"[{k}]") if params: return f"!{cmd.qualified_name} " + " ".join(params) return f"!{cmd.qualified_name}" except Exception: return f"!{cmd.name}" def _command_usage_slash(cmd: app_commands.Command) -> str: # Slash usage is mostly the path; options aren’t trivial to enumerate without ._params. # Do a safe best‑effort: try: parts = [f"/{cmd.name}"] # options (private API in discord.py); keep it safe and minimal: opts = [] for opt in getattr(cmd, "options", []) or []: n = getattr(opt, "name", "arg") req = getattr(opt, "required", False) opts.append(f"<{n}>" if req else f"[{n}]") if opts: parts.append(" " + " ".join(opts)) return "".join(parts) except Exception: return f"/{cmd.name}" def _gather_prefix_and_hybrid(bot: commands.Bot) -> List[Dict[str, Any]]: rows: List[Dict[str, Any]] = [] seen = set() for cmd in bot.commands: # Skip hidden if getattr(cmd, "hidden", False): continue ctype = "hybrid" if isinstance(cmd, commands.HybridCommand) else "prefix" is_mod, perms = _is_mod_command_prefix(cmd) row = { "type": ctype, "name": cmd.qualified_name, "help": (cmd.help or "").strip(), "brief": (cmd.brief or "").strip(), "usage": _command_usage_prefix(cmd), "cog": getattr(cmd.cog, "qualified_name", None) if getattr(cmd, "cog", None) else None, "module": getattr(cmd.callback, "__module__", None), "moderator_only": bool(is_mod), "required_permissions": perms, } key = ("px", row["name"]) if key not in seen: rows.append(row) seen.add(key) return rows def _walk_app_tree(node: Any, prefix: str = "") -> List[Tuple[str, app_commands.Command]]: """ Flatten app command tree (includes groups/subcommands) into (path, command) pairs. """ out: List[Tuple[str, app_commands.Command]] = [] if isinstance(node, app_commands.Command): out.append((f"{prefix}/{node.name}", node)) return out if isinstance(node, app_commands.Group): base = f"{prefix}/{node.name}" # Subcommands or nested groups for sub in list(getattr(node, "commands", []) or []): out.extend(_walk_app_tree(sub, base)) return out def _gather_slash(bot: commands.Bot) -> List[Dict[str, Any]]: rows: List[Dict[str, Any]] = [] try: # Local view of registered commands (no network call) cmds = bot.tree.get_commands() except Exception: cmds = [] for cmd in cmds: for path, leaf in _walk_app_tree(cmd, prefix=""): is_mod, perms = _is_mod_command_slash(leaf) row = { "type": "slash", "name": path, # includes group path "help": (leaf.description or "").strip(), "brief": "", "usage": _command_usage_slash(leaf), "cog": getattr(leaf, "binding", None).__class__.__name__ if getattr(leaf, "binding", None) else None, "module": getattr(leaf.callback, "__module__", None) if getattr(leaf, "callback", None) else None, "moderator_only": bool(is_mod), "required_permissions": perms, # Extras (discord.py 2.x): curation knobs per-command if authors set them "extras": dict(getattr(leaf, "extras", {}) or {}), # DM permission when available (discord.py exposes default permissions in command tree) "dm_permission": getattr(leaf, "dm_permission", None), } rows.append(row) return rows def build_command_schema(bot: commands.Bot) -> Dict[str, Any]: px = _gather_prefix_and_hybrid(bot) sl = _gather_slash(bot) all_rows = px + sl # Derive “moderator” flag if extras hint exists (in case checks missed it) for row in all_rows: try: ex = row.get("extras") or {} if isinstance(ex, dict) and ex.get("category", "").lower() in {"mod", "moderator", "staff"}: row["moderator_only"] = True except Exception: pass # Basic sections mods = [r for r in all_rows if r.get("moderator_only")] users = [r for r in all_rows if not r.get("moderator_only")] return { "title": "ShaiWatcher Commands", "count": len(all_rows), "sections": { "user": users, "moderator": mods, }, "all": all_rows, } # ----------------------------- # HTTP server + request handler # ----------------------------- _HTML = """