diff --git a/bot.py b/bot.py index 71a607e..bf81453 100644 --- a/bot.py +++ b/bot.py @@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice # Version consists of: # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update) -VERSION = "0.4.0.0.a1" +VERSION = "0.4.0.0.a2" # ---------- Env loading ---------- diff --git a/modules/docs_site/docs_site.py b/modules/docs_site/docs_site.py index a16d5fa..7283d32 100644 --- a/modules/docs_site/docs_site.py +++ b/modules/docs_site/docs_site.py @@ -323,42 +323,60 @@ def _safe_extras(obj: Any) -> Optional[Dict[str, Any]]: def _gather_slash(bot: commands.Bot) -> List[Dict[str, Any]]: rows: List[Dict[str, Any]] = [] + + # Collect paths from all scopes (global + each guild) + collected: List[Tuple[str, app_commands.Command, str]] = [] try: - cmds = bot.tree.get_commands() + for scope, top in _iter_all_app_commands(bot): + # walk each top-level into leaves + for path, leaf in _walk_app_tree(top, prefix=""): + collected.append((scope, leaf, path)) except Exception: traceback.print_exc() - cmds = [] - for cmd in cmds or []: - for path, leaf in _walk_app_tree(cmd, prefix=""): - try: - is_mod, perms = _is_mod_command_slash(leaf) - binding = getattr(leaf, "binding", None) - callback = getattr(leaf, "callback", None) - display = "/" + path.lstrip("/").replace("/", " ") - options = getattr(leaf, "options", None) or getattr(leaf, "parameters", None) or getattr(leaf, "_params", None) - usage_full = _command_usage_slash_like(path.lstrip("/").replace("/", " "), options) - - row = { - "type": "slash", - "name": path, # canonical '/group/sub' - "display_name": display, # shown without the leading '/' - "help": (getattr(leaf, "description", "") or "").strip(), - "brief": "", - "usage": usage_full, - "usage_prefix": None, - "usage_slash": usage_full, - "cog": binding.__class__.__name__ if binding else None, - "module": getattr(callback, "__module__", None) if callback else None, - "moderator_only": bool(is_mod), - "required_permissions": perms, - "extras": _safe_extras(leaf), - "dm_permission": getattr(leaf, "dm_permission", None), - } - rows.append(row) - except Exception: - traceback.print_exc() + # De-dupe by canonical path (e.g. "power/restart"), regardless of scope + seen_paths = set() + for scope, leaf, path in collected: + try: + canon = path.lstrip("/") # e.g. "power/restart" + if canon in seen_paths: continue + seen_paths.add(canon) + + is_mod, perms = _is_mod_command_slash(leaf) + binding = getattr(leaf, "binding", None) + callback = getattr(leaf, "callback", None) + + # UI shows "power restart" (title), but usage keeps "/power restart ..." + display = canon.replace("/", " ") + options = ( + getattr(leaf, "options", None) + or getattr(leaf, "parameters", None) + or getattr(leaf, "_params", None) + ) + usage_full = _command_usage_slash_like(display, options) + + row = { + "type": "slash", + "name": "/" + canon, # canonical with leading slash + "display_name": "/" + display, # shown without the leading slash in UI + "help": (getattr(leaf, "description", "") or "").strip(), + "brief": "", + "usage": usage_full, + "usage_prefix": None, + "usage_slash": usage_full, + "cog": binding.__class__.__name__ if binding else None, + "module": getattr(callback, "__module__", None) if callback else None, + "moderator_only": bool(is_mod), + "required_permissions": perms, + "extras": _safe_extras(leaf), + "dm_permission": getattr(leaf, "dm_permission", None), + } + rows.append(row) + except Exception: + traceback.print_exc() + continue + return rows @@ -517,6 +535,35 @@ def _merge_hybrid_slash(rows: List[Dict[str, Any]]) -> None: for i in sorted(to_remove, reverse=True): rows.pop(i) +# ============================= +# Global commands helper +# ============================= + +def _iter_all_app_commands(bot: commands.Bot): + """Yield (path, app_commands.Command) for global and per-guild trees.""" + out = [] + # Global + try: + for cmd in bot.tree.get_commands(): + out.append(("", cmd)) # empty scope tag + except Exception: + pass + + # Per-guild (guild-specific commands live here) + for g in list(getattr(bot, "guilds", []) or []): + try: + cmds = bot.tree.get_commands(guild=g) + except TypeError: + # older d.py variants accept Snowflake-like instead of Guild + try: + cmds = bot.tree.get_commands(guild=discord.Object(id=g.id)) + except Exception: + cmds = [] + except Exception: + cmds = [] + for cmd in cmds or []: + out.append((str(g.id), cmd)) # scope tag = guild id as string + return out # ============================= # Schema builder