# ╔══════════════════════════════════════════════════════════════════╗ # ║ ║ # ║ +-+-+-+-+-+-+-+-+ ║ # ║ |H|e|x|a|H|o|s|t| ║ # ║ +-+-+-+-+-+-+-+-+ ║ # ║ ║ # ║ © 2026 HexaHost — All Rights Reserved ║ # ║ ║ # ║ discord ── https://discord.gg/hexahost ║ # ║ github ── https://github.com/theoneandonlymace ║ # ║ ║ # ╚══════════════════════════════════════════════════════════════════╝ """Register slash (/) commands with Discord. Global sync alone can take up to ~1 hour to appear in clients. Guild sync makes them available immediately in every server the bot is in. """ from __future__ import annotations import asyncio import logging import os from typing import TYPE_CHECKING, Iterable, Optional import discord from discord import app_commands if TYPE_CHECKING: from discord.ext import commands logger = logging.getLogger("axiom.slash_sync") # Discord hard limit for top-level application commands (global or per-guild) MAX_TOP_LEVEL = 100 def _parse_guild_ids(raw: str | None) -> list[int]: if not raw: return [] out: list[int] = [] for part in raw.split(","): part = part.strip() if part.isdigit(): out.append(int(part)) return out def _tree_command_count(bot: "commands.Bot") -> int: return len(bot.tree.get_commands()) def _prune_tree_to_limit(bot: "commands.Bot", limit: int = MAX_TOP_LEVEL) -> list[str]: """Drop excess top-level app commands so Discord accepts the sync.""" commands_list = list(bot.tree.get_commands()) if len(commands_list) <= limit: return [] # Prefer keeping groups (more coverage) over single commands; drop from the end singles = [c for c in commands_list if not isinstance(c, app_commands.Group)] groups = [c for c in commands_list if isinstance(c, app_commands.Group)] ordered = groups + singles removed: list[str] = [] while len(ordered) > limit: cmd = ordered.pop() bot.tree.remove_command(cmd.name) removed.append(cmd.name) return removed async def sync_guild( bot: "commands.Bot", guild: discord.abc.Snowflake, *, copy_global: bool = True, ) -> list[app_commands.AppCommand]: if copy_global: bot.tree.copy_global_to(guild=guild) return await bot.tree.sync(guild=guild) async def sync_slash_commands( bot: "commands.Bot", *, guilds: Optional[Iterable[discord.abc.Snowflake]] = None, global_sync: bool = True, ) -> dict: """ Sync application commands. Env: SLASH_SYNC_GLOBAL=true|false (default true) SLASH_GUILD_IDS=id,id (optional; if set, only these guilds get instant sync) SLASH_SYNC_ALL_GUILDS=true (default true — sync every connected guild for instant /) """ env_global = os.getenv("SLASH_SYNC_GLOBAL", "true").strip().lower() in ("1", "true", "yes", "on") do_global = global_sync and env_global sync_all = os.getenv("SLASH_SYNC_ALL_GUILDS", "true").strip().lower() in ("1", "true", "yes", "on") env_guild_ids = _parse_guild_ids(os.getenv("SLASH_GUILD_IDS")) removed = _prune_tree_to_limit(bot) if removed: logger.warning( "Pruned %s top-level slash command(s) to stay under Discord's %s limit: %s", len(removed), MAX_TOP_LEVEL, ", ".join(removed[:20]) + ("…" if len(removed) > 20 else ""), ) print( f"\033[33m◈ Slash tree pruned by {len(removed)} (Discord max {MAX_TOP_LEVEL} top-level)\033[0m" ) local_count = _tree_command_count(bot) result: dict = { "local_tree": local_count, "global": None, "guilds": {}, "errors": [], "pruned": removed, } async def _try_sync(label: str, coro): try: synced = await coro return synced, None except Exception as e: # Drop invalid commands once and retry (common after bulk hybrid conversion) detail = str(e) logger.error("%s failed: %s", label, detail) dropped = [] for cmd in list(bot.tree.get_commands()): # Heuristic: remove cmds whose name appears in the error payload if cmd.name in detail or getattr(cmd, "qualified_name", "") in detail: bot.tree.remove_command(cmd.name) dropped.append(cmd.name) if dropped: print(f"\033[33m◈ Removed invalid slash cmd(s) after error: {', '.join(dropped)}\033[0m") try: synced = await coro return synced, None except Exception as e2: return None, f"{label}: {e2}" return None, f"{label}: {e}" if do_global: synced, err = await _try_sync("Global slash sync", bot.tree.sync()) if err: result["errors"].append(err) print(f"\033[31m◈ {err}\033[0m") else: result["global"] = len(synced or []) print(f"◈ Slash sync (global): {result['global']} command(s)") target_guilds: list[discord.abc.Snowflake] = [] if guilds is not None: target_guilds = list(guilds) elif env_guild_ids: target_guilds = [discord.Object(id=gid) for gid in env_guild_ids] elif sync_all: target_guilds = list(bot.guilds) for guild in target_guilds: gid = getattr(guild, "id", guild) async def _guild_sync(g=guild): bot.tree.copy_global_to(guild=g) return await bot.tree.sync(guild=g) synced, err = await _try_sync(f"Guild {gid} slash sync", _guild_sync()) if err: result["errors"].append(err) print(f"\033[31m◈ {err}\033[0m") else: result["guilds"][str(gid)] = len(synced or []) print(f"◈ Slash sync (guild {gid}): {result['guilds'][str(gid)]} command(s) — instant") await asyncio.sleep(0.35) prefix_count = len(list(bot.commands)) print( f"Synced Total {prefix_count} Client Commands and " f"{result.get('global') or local_count} Slash Commands" + (f" (guild-synced: {len(result['guilds'])})" if result["guilds"] else "") ) return result