# ╔══════════════════════════════════════════════════════════════════╗ # ║ ║ # ║ +-+-+-+-+-+-+-+-+ ║ # ║ |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. IMPORTANT: Never register the same commands both globally AND per-guild — Discord shows them twice in that server (e.g. /afk, /2048 duplicated). Modes (SLASH_SYNC_MODE): global — default. Sync once worldwide; clears leftover guild copies. guild — instant in connected servers only (no global registration). """ 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") MAX_TOP_LEVEL = 100 def _env_bool(key: str, default: bool) -> bool: raw = os.getenv(key) if raw is None: return default return raw.strip().lower() in ("1", "true", "yes", "on") def _parse_guild_ids(raw: str | None) -> list[int]: if not raw: return [] return [int(p.strip()) for p in raw.split(",") if p.strip().isdigit()] def _sync_mode() -> str: """ Resolve sync mode. - SLASH_SYNC_MODE=global|guild - Legacy: SLASH_SYNC_ALL_GUILDS / SLASH_GUILD_IDS imply guild mode if set true/nonempty while SLASH_SYNC_GLOBAL=false """ mode = (os.getenv("SLASH_SYNC_MODE") or "").strip().lower() if mode in ("global", "guild"): return mode # Legacy env compatibility — prefer no duplicates (global) unless guild-only requested if _env_bool("SLASH_SYNC_ALL_GUILDS", False) or _parse_guild_ids(os.getenv("SLASH_GUILD_IDS")): if not _env_bool("SLASH_SYNC_GLOBAL", True): return "guild" return "global" 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]: commands_list = list(bot.tree.get_commands()) if len(commands_list) <= limit: return [] 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 clear_guild_commands( bot: "commands.Bot", guild: discord.abc.Snowflake, ) -> None: """Remove guild-scoped slash commands so only global ones remain (fixes duplicates).""" bot.tree.clear_commands(guild=guild) await bot.tree.sync(guild=guild) 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 without creating global+guild duplicates. Env: SLASH_SYNC_MODE=global|guild (default: global) SLASH_GUILD_IDS=id,id (optional guild list for guild mode) SLASH_CLEAR_GUILD_COMMANDS=true (default true in global mode — removes old guild copies) """ mode = _sync_mode() clear_guild = _env_bool("SLASH_CLEAR_GUILD_COMMANDS", True) env_guild_ids = _parse_guild_ids(os.getenv("SLASH_GUILD_IDS")) removed = _prune_tree_to_limit(bot) if removed: 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 = { "mode": mode, "local_tree": local_count, "global": None, "guilds": {}, "cleared_guilds": [], "errors": [], "pruned": removed, } async def _try_sync(label: str, factory): try: return await factory(), None except Exception as e: logger.error("%s failed: %s", label, e) return None, f"{label}: {e}" # Resolve target guilds (for guild mode or for clearing duplicates) 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] else: target_guilds = list(bot.guilds) if mode == "guild": # Instant per-server only — do NOT also sync globally (would duplicate) for guild in target_guilds: gid = getattr(guild, "id", guild) def _make_factory(g=guild): async def _factory(): bot.tree.copy_global_to(guild=g) return await bot.tree.sync(guild=g) return _factory synced, err = await _try_sync(f"Guild {gid} slash sync", _make_factory()) 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)") await asyncio.sleep(0.35) else: # Global mode — one registry worldwide if global_sync: synced, err = await _try_sync("Global slash sync", lambda: 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)") # Remove guild-scoped copies left from older “sync both” runs if clear_guild and target_guilds: for guild in target_guilds: gid = getattr(guild, "id", guild) def _make_clear(g=guild): async def _factory(): await clear_guild_commands(bot, g) return [] return _factory _, err = await _try_sync(f"Clear guild {gid} slash cmds", _make_clear()) if err: result["errors"].append(err) print(f"\033[31m◈ {err}\033[0m") else: result["cleared_guilds"].append(str(gid)) print(f"◈ Cleared guild-scoped slash cmds for {gid} (removes duplicates)") 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"[mode={mode}]" ) return result