Implement slash command synchronization features in environment files and update command definitions to hybrid commands across various cogs. Enhance command registration for improved user experience and streamline command handling in the bot.

This commit is contained in:
TheOnlyMace
2026-07-21 22:41:41 +02:00
parent f15c869993
commit fc77f0a3c2
51 changed files with 382 additions and 153 deletions

184
bot/utils/slash_sync.py Normal file
View File

@@ -0,0 +1,184 @@
# ╔══════════════════════════════════════════════════════════════════╗
# ║ ║
# ║ +-+-+-+-+-+-+-+-+ ║
# ║ |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