diff --git a/.env.docker.example b/.env.docker.example index 14a35e2..7222c74 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -19,6 +19,11 @@ LAVALINK_PORT=13592 EMOJI_SYNC=false JISHAKU_ENABLED=false +# Slash (/) command sync — guild sync makes commands appear immediately +SLASH_SYNC_GLOBAL=true +SLASH_SYNC_ALL_GUILDS=true +# SLASH_GUILD_IDS= + # Traefik (optional override — default in compose: letsencrypt) # TRAEFIK_CERT_RESOLVER=letsencrypt diff --git a/bot/.env.example b/bot/.env.example index e4eb3ae..6e2622d 100644 --- a/bot/.env.example +++ b/bot/.env.example @@ -27,8 +27,11 @@ LAVALINK_PASSWORD="youshallnotpass" LAVALINK_SECURE="false" LAVALINK_PORT="13592" -# ── Emoji Sync ──────────────────────────────────────────────────────────────── -EMOJI_SYNC="false" +SLASH_SYNC_GLOBAL=true +# Instant / commands in every connected server (set false if you have many guilds and hit rate limits) +SLASH_SYNC_ALL_GUILDS=true +# Or limit instant sync to specific guild IDs (comma-separated). Overrides ALL_GUILDS when set. +# SLASH_GUILD_IDS= # ── API / Dashboard Backend ─────────────────────────────────────────────────── API_ENABLED="false" diff --git a/bot/Axiom.py b/bot/Axiom.py index 81ea579..527a212 100644 --- a/bot/Axiom.py +++ b/bot/Axiom.py @@ -33,6 +33,7 @@ from utils.Tools import * from utils.config import * from utils.emoji import SUCCESS, ERROR, TICK, CROSS, REACTION_TEST_EMOJIS from utils.sync_emojis import run_sync +from utils.slash_sync import sync_slash_commands import cogs @@ -99,21 +100,28 @@ async def on_ready(): # Sync application emojis on startup await run_sync(TOKEN) - async def sync_commands(): + # Register / slash commands (guild sync = instant; global can take up to ~1h) + if not getattr(client, "_slash_synced", False): + client._slash_synced = True try: - synced = await client.tree.sync() - all_commands = list(client.commands) - print(f"Synced Total {len(all_commands)} Client Commands and {len(synced)} Slash Commands") + await sync_slash_commands(client) except Exception as e: - print(f"Error syncing command tree: {e}") + print(f"\033[31mError syncing command tree: {e}\033[0m") - client.loop.create_task(sync_commands()) if SERVER_COUNT_CHANNEL_ID or USER_COUNT_CHANNEL_ID: client.loop.create_task(update_stats()) @client.event async def on_guild_join(guild: discord.Guild): + # Instant slash commands in the new server + try: + from utils.slash_sync import sync_guild + synced = await sync_guild(client, guild, copy_global=True) + print(f"◈ Slash sync for new guild {guild.id}: {len(synced)} command(s)") + except Exception as e: + print(f"Slash sync on guild join failed: {e}") + if not LOG_CHANNEL_ID: return log_channel = client.get_channel(LOG_CHANNEL_ID) @@ -155,7 +163,7 @@ async def on_command_completion(context: commands.Context) -> None: # --- Utility Commands --- -@client.command(name='spotify') +@client.hybrid_command(name='spotify') async def spotify(ctx: Context, user: discord.Member = None): """Shows what a user is listening to on Spotify.""" user = user or ctx.author @@ -176,7 +184,7 @@ async def spotify(ctx: Context, user: discord.Member = None): await ctx.send(embed=embed) -@client.command(name='makeinvite', aliases=['createinvite', 'makeinv']) +@client.hybrid_command(name='makeinvite', aliases=['createinvite', 'makeinv']) @commands.is_owner() async def make_invite(ctx: Context, guild_id: int = None): """Creates an invite for a specified server (owner only).""" @@ -206,7 +214,7 @@ async def make_invite(ctx: Context, guild_id: int = None): # --- Webhook Management Commands --- -@client.command(name='create_hook', aliases=['makehook']) +@client.hybrid_command(name='create_hook', aliases=['makehook']) @commands.has_permissions(administrator=True) async def create_hook(ctx: Context, *, name: str = None): """Creates a webhook in the current channel.""" @@ -228,7 +236,7 @@ async def create_hook(ctx: Context, *, name: str = None): await ctx.send(f"Webhook created: **{webhook.name}**\n||{webhook.url}||\n(I could not DM you the URL.)") -@client.command(name='delete_hook', aliases=['delhook']) +@client.hybrid_command(name='delete_hook', aliases=['delhook']) @commands.has_permissions(administrator=True) async def delete_hook(ctx: Context, webhook_url: str = None): """Deletes a webhook using its URL.""" @@ -244,7 +252,7 @@ async def delete_hook(ctx: Context, webhook_url: str = None): await ctx.send(f"{ERROR} Webhook not found or URL is invalid.") -@client.command(name='list_hooks', aliases=['hooks']) +@client.hybrid_command(name='list_hooks', aliases=['hooks']) @commands.has_permissions(administrator=True) async def list_hooks(ctx: Context): """Lists all webhooks in the current channel.""" @@ -262,7 +270,7 @@ async def list_hooks(ctx: Context): # --- Game Command --- -@client.command() +@client.hybrid_command() async def reaction(ctx: Context): """See how fast you can react to the correct emoji.""" emojis = ["🍪", "🎉", "🧋", "🍒", "🍑", "💸", "🌙", "💕"] diff --git a/bot/cogs/commands/Birthday.py b/bot/cogs/commands/Birthday.py index fa7b0d8..e787739 100644 --- a/bot/cogs/commands/Birthday.py +++ b/bot/cogs/commands/Birthday.py @@ -50,7 +50,7 @@ class Birthdays(commands.Cog): self.client = client self.check_birthdays.start() - @commands.command( + @commands.hybrid_command( name="birthdaysetup", help="Set up the birthday log channel and role.") @commands.has_permissions(administrator=True) @@ -69,7 +69,7 @@ class Birthdays(commands.Cog): await ctx.send(view=CV2("Birthday Setup", f"Birthday log channel set to {channel.mention} and birthday role set to {role.mention}.")) - @commands.command( + @commands.hybrid_command( name="setbirthday", help="Set your birthday.") @commands.guild_only() @@ -112,7 +112,7 @@ class Birthdays(commands.Cog): except asyncio.TimeoutError: await ctx.send(view=CV2("Error", "You took too long to respond. Please try again.")) - @commands.command( + @commands.hybrid_command( name="removebirthday", help="Remove your birthday.") @commands.guild_only() @@ -126,7 +126,7 @@ class Birthdays(commands.Cog): else: await ctx.send(view=CV2("Error", "You have no birthday set.")) - @commands.command( + @commands.hybrid_command( name="listbirthdays", help="List all members who have their birthday today.") @commands.guild_only() @@ -143,7 +143,7 @@ class Birthdays(commands.Cog): else: await ctx.send(view=CV2("Birthdays", "No birthdays today.")) - @commands.command( + @commands.hybrid_command( name="birthday", help="Check your birthday.") @commands.guild_only() diff --git a/bot/cogs/commands/Games.py b/bot/cogs/commands/Games.py index b365a19..7d50963 100644 --- a/bot/cogs/commands/Games.py +++ b/bot/cogs/commands/Games.py @@ -144,7 +144,7 @@ class Games(Cog): game = btn.BetaBattleShip(player1=ctx.author, player2=player) await game.start(ctx) - @commands.group(name="country-guesser", + @commands.hybrid_group(name="country-guesser", help="Guess name of the country by flag.", aliases=["guess", "guesser", "countryguesser"], usage="country-guesser") diff --git a/bot/cogs/commands/Invc.py b/bot/cogs/commands/Invc.py index 6f56777..fba30b6 100644 --- a/bot/cogs/commands/Invc.py +++ b/bot/cogs/commands/Invc.py @@ -36,7 +36,7 @@ class Invcrole(commands.Cog): ''') await db.commit() - @commands.group(name='vcrole', help="Vcrole Setup commands", invoke_without_command=True) + @commands.hybrid_group(name='vcrole', help="Vcrole Setup commands", invoke_without_command=True) @blacklist_check() @ignore_check() @commands.has_permissions(administrator=True) diff --git a/bot/cogs/commands/autoreact.py b/bot/cogs/commands/autoreact.py index c1b9e96..601262d 100644 --- a/bot/cogs/commands/autoreact.py +++ b/bot/cogs/commands/autoreact.py @@ -52,7 +52,7 @@ class AutoReaction(commands.Cog): cursor = await db.execute("SELECT 1 FROM autoreact WHERE guild_id = ? AND trigger = ?", (guild_id, trigger)) return await cursor.fetchone() - @commands.group(name="react", aliases=["autoreact"], help="Lists all subcommands of autoreact group.", invoke_without_command=True) + @commands.hybrid_group(name="react", aliases=["autoreact"], help="Lists all subcommands of autoreact group.", invoke_without_command=True) @blacklist_check() @ignore_check() @commands.cooldown(1, 4, commands.BucketType.user) diff --git a/bot/cogs/commands/autoresponder.py b/bot/cogs/commands/autoresponder.py index a0e081a..58c5e8e 100644 --- a/bot/cogs/commands/autoresponder.py +++ b/bot/cogs/commands/autoresponder.py @@ -46,7 +46,7 @@ class AutoResponder(commands.Cog): ''') await db.commit() - @commands.group(name="autoresponder", invoke_without_command=True, aliases=['ar'], help="Manage autoresponders in the server.") + @commands.hybrid_group(name="autoresponder", invoke_without_command=True, aliases=['ar'], help="Manage autoresponders in the server.") @blacklist_check() @ignore_check() @commands.cooldown(1, 5, commands.BucketType.user) diff --git a/bot/cogs/commands/autorole.py b/bot/cogs/commands/autorole.py index 10763a0..64b413f 100644 --- a/bot/cogs/commands/autorole.py +++ b/bot/cogs/commands/autorole.py @@ -92,7 +92,7 @@ class AutoRole(commands.Cog): - @commands.group(name="autorole", invoke_without_command=True) + @commands.hybrid_group(name="autorole", invoke_without_command=True) @commands.cooldown(1, 5, commands.BucketType.user) @commands.max_concurrency(1, per=commands.BucketType.default, wait=False) @commands.guild_only() diff --git a/bot/cogs/commands/blackjack.py b/bot/cogs/commands/blackjack.py index 9cd0d9c..6b8a225 100644 --- a/bot/cogs/commands/blackjack.py +++ b/bot/cogs/commands/blackjack.py @@ -123,7 +123,7 @@ class Blackjack(commands.Cog): total_sum += 1 return total_sum - @commands.command(aliases=['bj', 'blackjacks'], help="Play a simple game of blackjack.", usage="blackjack") + @commands.hybrid_command(aliases=['bj', 'blackjacks'], help="Play a simple game of blackjack.", usage="blackjack") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) diff --git a/bot/cogs/commands/blacklist.py b/bot/cogs/commands/blacklist.py index 6a113b2..304382a 100644 --- a/bot/cogs/commands/blacklist.py +++ b/bot/cogs/commands/blacklist.py @@ -207,7 +207,7 @@ class Blacklist(commands.Cog): await warning_message.delete(delay=3) break - @commands.group(name="blacklistword", aliases=["blword"], invoke_without_command=True) + @commands.hybrid_group(name="blacklistword", aliases=["blword"], invoke_without_command=True) @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) diff --git a/bot/cogs/commands/block.py b/bot/cogs/commands/block.py index 061306a..652fc50 100644 --- a/bot/cogs/commands/block.py +++ b/bot/cogs/commands/block.py @@ -54,7 +54,7 @@ class Block(commands.Cog): - @commands.group(name="blacklist", aliases=["bl"], invoke_without_command=True) + @commands.hybrid_group(name="blacklist", aliases=["bl"], invoke_without_command=True) @commands.is_owner() async def blacklist(self, ctx): if ctx.subcommand_passed is None: diff --git a/bot/cogs/commands/calc.py b/bot/cogs/commands/calc.py index ff2ea81..63701bf 100644 --- a/bot/cogs/commands/calc.py +++ b/bot/cogs/commands/calc.py @@ -122,7 +122,7 @@ class calculator(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.command(name='calculator', help='Starts a calculator session', aliases=['calc', 'calculate', 'math']) + @commands.hybrid_command(name='calculator', help='Starts a calculator session', aliases=['calc', 'calculate', 'math']) async def calculator(self, ctx): """Starts a new calculator session.""" # Ensure we pass the author to the view so it knows who triggered it diff --git a/bot/cogs/commands/counting.py b/bot/cogs/commands/counting.py index b7db219..7abb6e9 100644 --- a/bot/cogs/commands/counting.py +++ b/bot/cogs/commands/counting.py @@ -74,7 +74,7 @@ class Counting(commands.Cog): "**counting stats** — View current counting stats" )) - @commands.group(name="counting", invoke_without_command=True) + @commands.hybrid_group(name="counting", invoke_without_command=True) async def counting(self, ctx): if not self.is_enabled(ctx.guild.id): await self.not_enabled_embed(ctx) diff --git a/bot/cogs/commands/dms.py b/bot/cogs/commands/dms.py index b0f67ac..61652bd 100644 --- a/bot/cogs/commands/dms.py +++ b/bot/cogs/commands/dms.py @@ -81,7 +81,7 @@ class StaffDMCog(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.command(name="dmstaff") + @commands.hybrid_command(name="dmstaff") async def dm_staff(self, ctx, member: discord.Member, *, message: str): if ctx.author.id not in STAFF_IDS: view = PermissionErrorView() diff --git a/bot/cogs/commands/emergency.py b/bot/cogs/commands/emergency.py index 5660525..1e60d44 100644 --- a/bot/cogs/commands/emergency.py +++ b/bot/cogs/commands/emergency.py @@ -406,7 +406,7 @@ class Emergency(commands.Cog): ) as cursor: return await cursor.fetchone() is not None - @commands.group(name="emergency", aliases=["emg"], invoke_without_command=True) + @commands.hybrid_group(name="emergency", aliases=["emg"], invoke_without_command=True) @blacklist_check() @ignore_check() @commands.cooldown(1, 4, commands.BucketType.user) @@ -614,7 +614,7 @@ class Emergency(commands.Cog): roles = await cursor.fetchall() await ctx.reply(view=RoleListView(roles, is_auth)) - @commands.command(name="emergencysituation", aliases=["emgs"]) + @commands.hybrid_command(name="emergencysituation", aliases=["emgs"]) @blacklist_check() @ignore_check() @commands.cooldown(1, 40, commands.BucketType.user) @@ -736,7 +736,7 @@ class Emergency(commands.Cog): await anti.commit() await proc_msg.delete() - @commands.command(name="emergencyrestore", aliases=["emgrestore"]) + @commands.hybrid_command(name="emergencyrestore", aliases=["emgrestore"]) @blacklist_check() @ignore_check() @commands.cooldown(1, 30, commands.BucketType.user) diff --git a/bot/cogs/commands/encryption.py b/bot/cogs/commands/encryption.py index 10540bc..eea3770 100644 --- a/bot/cogs/commands/encryption.py +++ b/bot/cogs/commands/encryption.py @@ -88,12 +88,12 @@ class encryption(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.group(invoke_without_command=True) + @commands.hybrid_group(invoke_without_command=True) async def encode(self, ctx): """All encode methods""" await ctx.send_help(ctx.command) - @commands.group(invoke_without_command=True) + @commands.hybrid_group(invoke_without_command=True) async def decode(self, ctx): """All decode methods""" await ctx.send_help(ctx.command) @@ -204,7 +204,7 @@ class encryption(commands.Cog): except Exception: await ctx.send(view=DecodeErrorView("ASCII85")) - @commands.command(name="password") + @commands.hybrid_command(name="password") async def password(self, ctx): """Generates a random secure password for you""" if hasattr(ctx, "guild") and ctx.guild is not None: diff --git a/bot/cogs/commands/extra.py b/bot/cogs/commands/extra.py index f2f904d..bc41dcf 100644 --- a/bot/cogs/commands/extra.py +++ b/bot/cogs/commands/extra.py @@ -197,7 +197,7 @@ class Extra(commands.Cog): - @commands.command(name="uptime", description="Shows the Bot's Uptime.") + @commands.hybrid_command(name="uptime", description="Shows the Bot's Uptime.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -447,7 +447,7 @@ class Extra(commands.Cog): - @commands.command(name="boostcount", + @commands.hybrid_command(name="boostcount", help="Shows boosts count", usage="boosts", aliases=["bco"], @@ -788,7 +788,7 @@ class Extra(commands.Cog): - @commands.command(name="joined-at", + @commands.hybrid_command(name="joined-at", help="Shows when a user joined", usage="joined-at [user]", with_app_command=True) @@ -799,7 +799,7 @@ class Extra(commands.Cog): joined = ctx.author.joined_at.strftime("%a, %d %b %Y %I:%M %p") await ctx.send(view=CV2("joined-at", f"**`{joined}`**")) - @commands.command(name="github", usage="github [search]") + @commands.hybrid_command(name="github", usage="github [search]") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -906,7 +906,7 @@ class Extra(commands.Cog): - @commands.command(name="permissions", aliases= ["perms"], + @commands.hybrid_command(name="permissions", aliases= ["perms"], help="Check and list the key permissions of a specific user", usage="perms ", with_app_command=True) diff --git a/bot/cogs/commands/fastgreet.py b/bot/cogs/commands/fastgreet.py index d0fa73a..8c232b8 100644 --- a/bot/cogs/commands/fastgreet.py +++ b/bot/cogs/commands/fastgreet.py @@ -35,7 +35,7 @@ class FastGreet(commands.Cog): ) """) - @commands.command(name="fastgreet_add") + @commands.hybrid_command(name="fastgreet_add") @commands.has_permissions(administrator=True) async def add_greet_channel(self, ctx, channel: discord.TextChannel): with sqlite3.connect(DB_PATH) as conn: @@ -45,7 +45,7 @@ class FastGreet(commands.Cog): """, (ctx.guild.id, channel.id)) await ctx.send(f"✅ {channel.mention} added as a greet channel.") - @commands.command(name="fastgreet_remove") + @commands.hybrid_command(name="fastgreet_remove") @commands.has_permissions(administrator=True) async def remove_greet_channel(self, ctx, channel: discord.TextChannel): with sqlite3.connect(DB_PATH) as conn: @@ -54,7 +54,7 @@ class FastGreet(commands.Cog): """, (ctx.guild.id, channel.id)) await ctx.send(f"❌ {channel.mention} removed from greet channels.") - @commands.command(name="fastgreet_list") + @commands.hybrid_command(name="fastgreet_list") async def list_greet_channels(self, ctx): with sqlite3.connect(DB_PATH) as conn: cursor = conn.execute(""" diff --git a/bot/cogs/commands/fun.py b/bot/cogs/commands/fun.py index ff7de2f..58435b3 100644 --- a/bot/cogs/commands/fun.py +++ b/bot/cogs/commands/fun.py @@ -56,58 +56,58 @@ class Fun(commands.Cog): async def meter_command(self, ctx, title, user, text): await ctx.send(view=CV2(title, text)) - @commands.command(name="shipp") + @commands.hybrid_command(name="shipp") @blacklist_check() @ignore_check() async def shipp(self, ctx, user1: discord.Member, user2: discord.Member): percentage = random.randint(0, 100) await ctx.send(view=CV2(f"{self.random_emoji()} Ship Result", f"**{user1.mention} x {user2.mention} = {percentage}% Love**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def hug(self, ctx, user: discord.Member): await self.action_command(ctx, user, "hug") - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def kiss(self, ctx, user: discord.Member): await self.action_command(ctx, user, "kiss") - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def pat(self, ctx, user: discord.Member): await self.action_command(ctx, user, "pat") - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def slap(self, ctx, user: discord.Member): await self.action_command(ctx, user, "slap") - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def tickle(self, ctx, user: discord.Member): await self.action_command(ctx, user, "tickle") - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def coinflip(self, ctx): result = random.choice(["Heads", "Tails"]) await ctx.send(view=CV2("🪙 Coin Flip", f"**Result: {result}**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def dice(self, ctx): result = random.randint(1, 6) await ctx.send(view=CV2("🎲 Dice Roll", f"**You rolled a {result}!**")) - @commands.command(name="8ball") + @commands.hybrid_command(name="8ball") @blacklist_check() @ignore_check() async def eight_ball(self, ctx, *, question: str): @@ -116,7 +116,7 @@ class Fun(commands.Cog): "Don't count on it.", "My sources say no.", "Very doubtful."] await ctx.send(view=CV2("🎱 Magic 8Ball", f"**Q:** {question}\n**A:** {random.choice(responses)}")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def roast(self, ctx, user: discord.Member): @@ -127,56 +127,56 @@ class Fun(commands.Cog): ] await ctx.send(view=CV2("🔥 Roast Time", random.choice(roasts))) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def iq(self, ctx, user: discord.Member = None): user = user or ctx.author await ctx.send(view=CV2("🧠 IQ Test", f"**{user.mention} has an IQ of {random.randint(50, 200)}!**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def dumb(self, ctx, user: discord.Member = None): user = user or ctx.author await ctx.send(view=CV2("🤪 Dumbness Test", f"**{user.mention} is {random.randint(0, 100)}% dumb!**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def simprate(self, ctx, user: discord.Member = None): user = user or ctx.author await ctx.send(view=CV2("😳 Simp Rate", f"**{user.mention} is {random.randint(0, 100)}% simp!**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def toxic(self, ctx, user: discord.Member = None): user = user or ctx.author await ctx.send(view=CV2("☠️ Toxic Meter", f"**{user.mention} is {random.randint(0, 100)}% toxic!**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def intelligence(self, ctx, user: discord.Member = None): user = user or ctx.author await ctx.send(view=CV2("🧠 Intelligence Meter", f"**{user.mention} has {random.randint(0, 200)} IQ Points!**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def genius(self, ctx, user: discord.Member = None): user = user or ctx.author await ctx.send(view=CV2("🤓 Genius Rate", f"**{user.mention} is {random.randint(0, 100)}% genius!**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def brainrate(self, ctx, user: discord.Member = None): user = user or ctx.author await ctx.send(view=CV2("🧠 Brain Power", f"**{user.mention} is using {random.randint(0, 100)}% of their brain!**")) - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() async def howhot(self, ctx, user: discord.Member = None): diff --git a/bot/cogs/commands/general.py b/bot/cogs/commands/general.py index d74b6a5..5daf0ad 100644 --- a/bot/cogs/commands/general.py +++ b/bot/cogs/commands/general.py @@ -208,7 +208,7 @@ class General(commands.Cog): await msg.add_reaction(CROSS) - @commands.command(name="users", help=f"checks total users of {BotName}.") + @commands.hybrid_command(name="users", help=f"checks total users of {BotName}.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -251,7 +251,7 @@ class General(commands.Cog): except: pass - @commands.command(name="rickroll", + @commands.hybrid_command(name="rickroll", help="Detects if provided url is a rick-roll", usage="Rickroll ") @blacklist_check() @@ -271,7 +271,7 @@ class General(commands.Cog): title = "Rick Roll {} in webpage".format("was found" if rickRoll else "was not found") await ctx.reply(view=CV2(title, "🎵 Never gonna give you up..." if rickRoll else "✅ Safe to click!"), mention_author=True) - @commands.command(name="hash", + @commands.hybrid_command(name="hash", help="Hashes provided text with provided algorithm") @blacklist_check() @ignore_check() @@ -297,7 +297,7 @@ class General(commands.Cog): hash_lines = f"**{algorithm}:** `{algos[algorithm.lower()]}`" await ctx.reply(view=CV2(f"Hashed \"{message}\"", hash_lines), mention_author=True) - @commands.command( + @commands.hybrid_command( name="invite", aliases=['invite-bot'], description="Get Support & Bot invite link!" diff --git a/bot/cogs/commands/ignore.py b/bot/cogs/commands/ignore.py index aeb2d0b..23a6127 100644 --- a/bot/cogs/commands/ignore.py +++ b/bot/cogs/commands/ignore.py @@ -118,7 +118,7 @@ class Ignore(commands.Cog): ) await db.commit() - @commands.group( + @commands.hybrid_group( name="ignore", help="Manage ignored commands, channels, users, and bypassed users.", invoke_without_command=True, diff --git a/bot/cogs/commands/image.py b/bot/cogs/commands/image.py index 132b588..3ed7bda 100644 --- a/bot/cogs/commands/image.py +++ b/bot/cogs/commands/image.py @@ -53,22 +53,22 @@ class ImageCommands(commands.Cog): else: await ctx.send(view=CV2("❌ Error", f"No image found for {title.lower()}.")) - @commands.command(name="boy") + @commands.hybrid_command(name="boy") async def boy_image(self, ctx): url = await self.fetch_pexels_image("handsome boy") await self.send_image_view(ctx, "👦 Boy Pic", url) - # @commands.command(name="girl") + # @commands.hybrid_command(name="girl") # async def girl_image(self, ctx): # url = await self.fetch_pexels_image("beautiful girl") # await self.send_image_view(ctx, "👧 Girl Pic", url) - @commands.command(name="couple") + @commands.hybrid_command(name="couple") async def couple_image(self, ctx): url = await self.fetch_pexels_image("romantic couple") await self.send_image_view(ctx, "💑 Couple Pic", url) - @commands.command(name="anime") + @commands.hybrid_command(name="anime") async def anime_image(self, ctx): url = await self.fetch_waifu_image("waifu") await self.send_image_view(ctx, "🧚 Anime Waifu", url) diff --git a/bot/cogs/commands/j2c.py b/bot/cogs/commands/j2c.py index e7ece01..145de6a 100644 --- a/bot/cogs/commands/j2c.py +++ b/bot/cogs/commands/j2c.py @@ -190,7 +190,7 @@ class JoinToCreate(commands.Cog): print(f"Error in J2C on_ready: {e}") continue - @commands.command(name='j2csetup') + @commands.hybrid_command(name='j2csetup') @commands.has_permissions(administrator=True) async def setup_private_channels(self, ctx): if ctx.guild.id in self.setup_data: @@ -225,7 +225,7 @@ class JoinToCreate(commands.Cog): await ctx.send(view=CV2("✅ Success", f"J2C system setup complete! Join {join_channel.mention} to create a private VC.")) - @commands.command(name='j2creset') + @commands.hybrid_command(name='j2creset') @commands.has_permissions(administrator=True) async def reset_private_channels(self, ctx): if ctx.guild.id not in self.setup_data: diff --git a/bot/cogs/commands/jail.py b/bot/cogs/commands/jail.py index 459f0a4..37f1be6 100644 --- a/bot/cogs/commands/jail.py +++ b/bot/cogs/commands/jail.py @@ -129,7 +129,7 @@ class Jail(commands.Cog): ) await log_channel.send(view=CV2("🔓 Member Unjailed", desc)) - @commands.command(name="jail") + @commands.hybrid_command(name="jail") @commands.has_permissions(manage_roles=True) async def jail(self, ctx, member: discord.Member, duration: str = None, *, reason="No reason provided"): jail_role_id = self.get_setting(ctx.guild.id, "jail_role") @@ -182,13 +182,13 @@ class Jail(commands.Cog): ) await log_channel.send(view=CV2("🔒 Member Jailed", desc)) - @commands.command(name="unjail") + @commands.hybrid_command(name="unjail") @commands.has_permissions(manage_roles=True) async def unjail(self, ctx, member: discord.Member): await self.unjail_member(ctx.guild, member) await ctx.send(view=CV2("✅ Success", f"{member.mention} has been unjailed.")) - @commands.command(name="jailhistory") + @commands.hybrid_command(name="jailhistory") async def jailhistory(self, ctx, member: discord.Member): cursor = self.conn.execute(""" SELECT reason, jailed_at, duration, mod_id FROM jailed diff --git a/bot/cogs/commands/joindm.py b/bot/cogs/commands/joindm.py index ab2aa09..ebfccb9 100644 --- a/bot/cogs/commands/joindm.py +++ b/bot/cogs/commands/joindm.py @@ -37,7 +37,7 @@ class joindm(commands.Cog): with open(jsondb_path('joindm_messages.json'), 'w') as f: json.dump(self.joindm_messages, f) - @commands.group(invoke_without_command=True) + @commands.hybrid_group(invoke_without_command=True) @commands.has_permissions(administrator=True) async def joindm(self, ctx): # Display the current join DM message diff --git a/bot/cogs/commands/leveling.py b/bot/cogs/commands/leveling.py index f01cc06..82c809a 100644 --- a/bot/cogs/commands/leveling.py +++ b/bot/cogs/commands/leveling.py @@ -293,7 +293,7 @@ class Leveling (commands .Cog ): self .last_level_cache ={} self .db_path =db_path("leveling.db") - @commands.group (name ="level",invoke_without_command =True ,description ="Leveling system") + @commands.hybrid_group (name ="level",invoke_without_command =True ,description ="Leveling system") async def level (self ,ctx ): """Main leveling command""" if ctx .invoked_subcommand is None : diff --git a/bot/cogs/commands/messages.py b/bot/cogs/commands/messages.py index 6e54ed8..e9dda61 100644 --- a/bot/cogs/commands/messages.py +++ b/bot/cogs/commands/messages.py @@ -81,7 +81,7 @@ class Messages(commands.Cog): conn.commit() conn.close() - @commands.command(name="messages", aliases=["msg"]) + @commands.hybrid_command(name="messages", aliases=["msg"]) async def messages(self, ctx, member: discord.Member = None): member = member or ctx.author today = datetime.utcnow().strftime("%Y-%m-%d") diff --git a/bot/cogs/commands/msgpack.py b/bot/cogs/commands/msgpack.py index 45cea5f..ece44ad 100644 --- a/bot/cogs/commands/msgpack.py +++ b/bot/cogs/commands/msgpack.py @@ -21,7 +21,7 @@ class Messagespack(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.command(name="addmessages", aliases=["addmsg"]) + @commands.hybrid_command(name="addmessages", aliases=["addmsg"]) @commands.has_permissions(manage_messages=True) async def addmessages(self, ctx, member: discord.Member, amount: int): if amount <= 0: @@ -46,7 +46,7 @@ class Messagespack(commands.Cog): conn.close() await ctx.send(f"Added {amount} messages to {member.mention} for today.") - @commands.command(name="removemessages", aliases=["removemsg"]) + @commands.hybrid_command(name="removemessages", aliases=["removemsg"]) @commands.has_permissions(manage_messages=True) async def removemessages(self, ctx, member: discord.Member, amount: int): if amount <= 0: @@ -70,7 +70,7 @@ class Messagespack(commands.Cog): await ctx.send(f"{member.mention} has no messages recorded for today.") conn.close() - @commands.command(name="clearmessage", aliases=["clearmsg"]) + @commands.hybrid_command(name="clearmessage", aliases=["clearmsg"]) @commands.has_permissions(manage_messages=True) async def clearmessage(self, ctx, member: discord.Member): conn = sqlite3.connect(db_path("messages.db")) diff --git a/bot/cogs/commands/music.py b/bot/cogs/commands/music.py index d0d710e..d1e4e14 100644 --- a/bot/cogs/commands/music.py +++ b/bot/cogs/commands/music.py @@ -585,7 +585,7 @@ class Music(commands.Cog): bar = '█' * filled_length + '░' * (length - filled_length) return bar - @commands.command(name="play", aliases=['p'], usage="play ", help="Plays a song or playlist.") + @commands.hybrid_command(name="play", aliases=['p'], usage="play ", help="Plays a song or playlist.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -594,7 +594,7 @@ class Music(commands.Cog): await self.play_source(ctx, query) - @commands.command(name="search", usage="search ", help="Searches music from multiple platforms.") + @commands.hybrid_command(name="search", usage="search ", help="Searches music from multiple platforms.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -606,7 +606,7 @@ class Music(commands.Cog): await ctx.send(view=PlatformSelectView(ctx, query)) - @commands.command(name="nowplaying", aliases=["nop"], usage="nowplaying", help="Shows the info about current playing song.") + @commands.hybrid_command(name="nowplaying", aliases=["nop"], usage="nowplaying", help="Shows the info about current playing song.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -659,7 +659,7 @@ class Music(commands.Cog): await ctx.send(view=view) - @commands.command(name="autoplay", usage="autoplay", help="Toggles autoplay mode.") + @commands.hybrid_command(name="autoplay", usage="autoplay", help="Toggles autoplay mode.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -679,7 +679,7 @@ class Music(commands.Cog): ) await ctx.send(view=CV2(f"{TICK} Autoplay {'enabled' if vc.autoplay == wavelink.AutoPlayMode.enabled else 'disabled'} by {ctx.author.mention}.")) - @commands.command(name="loop", usage="loop", help="Toggles loop mode.") + @commands.hybrid_command(name="loop", usage="loop", help="Toggles loop mode.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -700,7 +700,7 @@ class Music(commands.Cog): await ctx.send(view=CV2("I'm not connected to a voice channel.")) - @commands.command(name="pause", usage="pause", help="Pauses the current song.") + @commands.hybrid_command(name="pause", usage="pause", help="Pauses the current song.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -721,7 +721,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2(f"{WARNING} Nothing is playing or already paused.")) - @commands.command(name="resume", usage="resume", help="Resumes the paused song.") + @commands.hybrid_command(name="resume", usage="resume", help="Resumes the paused song.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -742,7 +742,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2("Player is not paused.")) - @commands.command(name="skip", usage="skip", help="Skips the current song.") + @commands.hybrid_command(name="skip", usage="skip", help="Skips the current song.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -767,7 +767,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2(f"{WARNING} No song is playing or in the queue to skip.")) - @commands.command(name="shuffle", usage="shuffle", help="Shuffles the queue.") + @commands.hybrid_command(name="shuffle", usage="shuffle", help="Shuffles the queue.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -787,7 +787,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2("Queue is empty.")) - @commands.command(name="stop", usage="stop", help="Stops the current song and clears the queue.") + @commands.hybrid_command(name="stop", usage="stop", help="Stops the current song and clears the queue.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -810,7 +810,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2("Nothing is playing to stop.")) - @commands.command(name="volume", aliases=["vol"], usage="volume ", help="Sets the volume of the player.") + @commands.hybrid_command(name="volume", aliases=["vol"], usage="volume ", help="Sets the volume of the player.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -834,7 +834,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2("Bot is not connected to a voice channel.")) - @commands.command(name="queue", usage="queue", help="Shows the current queue.") + @commands.hybrid_command(name="queue", usage="queue", help="Shows the current queue.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -860,7 +860,7 @@ class Music(commands.Cog): ctx=ctx) await paginator.paginate() - @commands.command(name="clearqueue", usage="clearqueue", help="Clears the queue.") + @commands.hybrid_command(name="clearqueue", usage="clearqueue", help="Clears the queue.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -881,7 +881,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2("No queue to clear.")) - @commands.command(name="replay", usage="replay", help="Replays the current song.") + @commands.hybrid_command(name="replay", usage="replay", help="Replays the current song.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -902,7 +902,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2("No track is currently playing.")) - @commands.command(name="join", aliases=["connect"], usage="join", help="Joins the voice channel.") + @commands.hybrid_command(name="join", aliases=["connect"], usage="join", help="Joins the voice channel.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -933,7 +933,7 @@ class Music(commands.Cog): else: await ctx.send(view=CV2("Bot is not connected to any voice channel.")) - @commands.command(name="seek", usage="seek ", help="Seeks to a specific percentage of the song.") + @commands.hybrid_command(name="seek", usage="seek ", help="Seeks to a specific percentage of the song.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) diff --git a/bot/cogs/commands/nitro.py b/bot/cogs/commands/nitro.py index 897789e..506c486 100644 --- a/bot/cogs/commands/nitro.py +++ b/bot/cogs/commands/nitro.py @@ -28,7 +28,7 @@ class Nitro(commands.Cog): ctx = await self.bot.get_context(message) await self.bot.invoke(ctx) - @commands.command(name="nitro") + @commands.hybrid_command(name="nitro") async def nitro(self, ctx): embed = discord.Embed(color=0x2B2D31) embed.add_field( diff --git a/bot/cogs/commands/notify.py b/bot/cogs/commands/notify.py index 4f6a3cf..83c05b9 100644 --- a/bot/cogs/commands/notify.py +++ b/bot/cogs/commands/notify.py @@ -34,7 +34,7 @@ class NotifCommands(commands.Cog): channel_id INTEGER NOT NULL)""") await db.commit() - @commands.group(invoke_without_command=True) + @commands.hybrid_group(invoke_without_command=True) async def setnotif(self, ctx): view = CV2( "Notification Commands", diff --git a/bot/cogs/commands/np.py b/bot/cogs/commands/np.py index 2ffc81e..a820b54 100644 --- a/bot/cogs/commands/np.py +++ b/bot/cogs/commands/np.py @@ -465,7 +465,7 @@ class NoPrefix(commands.Cog): await ctx.reply(view=embed) - @commands.group(name="autonp", help="Manage auto no-prefix for partner guilds.") + @commands.hybrid_group(name="autonp", help="Manage auto no-prefix for partner guilds.") @commands.is_owner() async def autonp(self, ctx): if ctx.invoked_subcommand is None: diff --git a/bot/cogs/commands/owner.py b/bot/cogs/commands/owner.py index 61d4dbf..f45c1b1 100644 --- a/bot/cogs/commands/owner.py +++ b/bot/cogs/commands/owner.py @@ -160,7 +160,7 @@ class Owner(commands.Cog): async with db.execute('SELECT id FROM staff') as cursor: self.staff = {row[0] for row in await cursor.fetchall()} - @commands.command(name="staff_add", aliases=["staffadd", "addstaff"], help="Adds a user to the staff list.") + @commands.hybrid_command(name="staff_add", aliases=["staffadd", "addstaff"], help="Adds a user to the staff list.") @commands.is_owner() async def staff_add(self, ctx, user: discord.User): if user.id in self.staff: @@ -174,7 +174,7 @@ class Owner(commands.Cog): sonu2 = discord.Embed(title=f"{TICK} Success", description=f"Added {user} to the staff list.", color=0xFF0000) await ctx.reply(embed=sonu2, mention_author=False) - @commands.command(name="staff_remove", aliases=["staffremove", "removestaff"], help="Removes a user from the staff list.") + @commands.hybrid_command(name="staff_remove", aliases=["staffremove", "removestaff"], help="Removes a user from the staff list.") @commands.is_owner() async def staff_remove(self, ctx, user: discord.User): if user.id not in self.staff: @@ -188,7 +188,7 @@ class Owner(commands.Cog): sonu2 = discord.Embed(title=f"{TICK} Success", description=f"Removed {user} from the staff list.", color=0xFF0000) await ctx.reply(embed=sonu2, mention_author=False) - @commands.command(name="staff_list", aliases=["stafflist", "liststaff", "staffs"], help="Lists all staff members.") + @commands.hybrid_command(name="staff_list", aliases=["stafflist", "liststaff", "staffs"], help="Lists all staff members.") @commands.is_owner() async def staff_list(self, ctx): if not self.staff: @@ -202,7 +202,7 @@ class Owner(commands.Cog): sonu = discord.Embed(title=f"{TICK} {BotName} Staffs", description=f"\n{staff_display}", color=0xFF0000) await ctx.send(embed=sonu) - @commands.command(name="slist") + @commands.hybrid_command(name="slist") @commands.check(is_owner_or_staff) async def _slist(self, ctx): servers = sorted(self.client.guilds, key=lambda g: g.member_count, reverse=True) @@ -220,7 +220,7 @@ class Owner(commands.Cog): await paginator.paginate() - @commands.command(name="mutuals", aliases=["mutual"]) + @commands.hybrid_command(name="mutuals", aliases=["mutual"]) @commands.is_owner() async def mutuals(self, ctx, user: discord.User): guilds = [guild for guild in self.client.guilds if user in guild.members] @@ -237,7 +237,7 @@ class Owner(commands.Cog): ctx=ctx) await paginator.paginate() - @commands.command(name="getinvite", aliases=["gi", "guildinvite"]) + @commands.hybrid_command(name="getinvite", aliases=["gi", "guildinvite"]) @commands.is_owner() async def getinvite(self, ctx: Context, guild= discord.Guild): if not guild: @@ -272,13 +272,13 @@ class Owner(commands.Cog): await ctx.send("Forbidden.") - @commands.command(name="reload", help="Restarts the client.") + @commands.hybrid_command(name="reload", help="Restarts the client.") @commands.is_owner() async def _restart(self, ctx: Context): await ctx.reply(f"{TICK} | **Successfully Restarting {BotName} It Takes 10 seconds**") restart_program() - @commands.command(name="sync", help="Syncs all database.") + @commands.hybrid_command(name="sync", help="Syncs all database.") @commands.is_owner() async def _sync(self, ctx): await ctx.reply("Syncing...", mention_author=False) @@ -300,8 +300,35 @@ class Owner(commands.Cog): with open('config.json', 'w') as f: json.dump(data, f, indent=4) + @commands.hybrid_command(name="syncslash", help="Re-register all slash (/) commands with Discord.") + @commands.is_owner() + async def syncslash(self, ctx, scope: str = "all"): + """Force-sync slash commands. scope: all | global | guild""" + if ctx.interaction: + await ctx.defer(ephemeral=True) + from utils.slash_sync import sync_slash_commands - @commands.command(name="owners") + scope = (scope or "all").lower().strip() + if scope == "global": + result = await sync_slash_commands(self.client, guilds=[], global_sync=True) + elif scope == "guild": + if not ctx.guild: + return await ctx.send("Use this in a server, or pass scope `all` / `global`.") + result = await sync_slash_commands( + self.client, guilds=[ctx.guild], global_sync=False + ) + else: + result = await sync_slash_commands(self.client) + + guild_n = len(result.get("guilds") or {}) + err_n = len(result.get("errors") or []) + await ctx.send( + f"{TICK} Slash sync done — tree `{result.get('local_tree')}`, " + f"global `{result.get('global')}`, guilds `{guild_n}`, errors `{err_n}`." + ) + + + @commands.hybrid_command(name="owners") @commands.is_owner() async def own_list(self, ctx): nplist = OWNER_IDS @@ -324,7 +351,7 @@ class Owner(commands.Cog): - @commands.command() + @commands.hybrid_command() @commands.is_owner() async def dm(self, ctx, user: discord.User, *, message: str): """ DM the user of your choice """ @@ -336,7 +363,7 @@ class Owner(commands.Cog): - @commands.group() + @commands.hybrid_group() @commands.is_owner() async def change(self, ctx): if ctx.invoked_subcommand is None: @@ -357,7 +384,7 @@ class Owner(commands.Cog): await ctx.send(err) - @commands.command(name="ownerban", aliases=["forceban", "dna"]) + @commands.hybrid_command(name="ownerban", aliases=["forceban", "dna"]) @commands.is_owner() async def _ownerban(self, ctx: Context, user_id: int, *, reason: str = "No reason provided"): @@ -391,7 +418,7 @@ class Owner(commands.Cog): await ctx.reply("User not found in this guild.", mention_author=False, delete_after=3) await ctx.message.delete() - @commands.command(name="ownerunban", aliases=["forceunban"]) + @commands.hybrid_command(name="ownerunban", aliases=["forceunban"]) @commands.is_owner() async def _ownerunban(self, ctx: Context, user_id: int, *, reason: str = "No reason provided"): user = self.client.get_user(user_id) @@ -423,7 +450,7 @@ class Owner(commands.Cog): - @commands.command(name="globalunban") + @commands.hybrid_command(name="globalunban") @commands.is_owner() async def globalunban(self, ctx: Context, user: discord.User): success_guilds = [] @@ -447,7 +474,7 @@ class Owner(commands.Cog): await ctx.reply(f"{success_message}\n{error_message}", mention_author=False) - @commands.command(name="guildban") + @commands.hybrid_command(name="guildban") @commands.is_owner() async def guildban(self, ctx: Context, guild_id: int, user_id: int, *, reason: str = "No reason provided"): guild = self.client.get_guild(guild_id) @@ -467,7 +494,7 @@ class Owner(commands.Cog): else: await ctx.reply(f"User not found in the specified guild {guild.name}.", mention_author=False) - @commands.command(name="guildunban") + @commands.hybrid_command(name="guildunban") @commands.is_owner() async def guildunban(self, ctx: Context, guild_id: int, user_id: int, *, reason: str = "No reason provided"): guild = self.client.get_guild(guild_id) @@ -492,7 +519,7 @@ class Owner(commands.Cog): await ctx.reply(f"An error occurred while unbanning user ID {user_id} in {guild.name}: {str(e)}", mention_author=False) - @commands.command(name="leaveguild", aliases=["leavesv"]) + @commands.hybrid_command(name="leaveguild", aliases=["leavesv"]) @commands.is_owner() async def leave_guild(self, ctx, guild_id: int): guild = self.client.get_guild(guild_id) @@ -503,7 +530,7 @@ class Owner(commands.Cog): await guild.leave() await ctx.send(f"Left the guild: {guild.name} ({guild.id})") - @commands.command(name="guildinfo") + @commands.hybrid_command(name="guildinfo") @commands.check(is_owner_or_staff) async def guild_info(self, ctx, guild_id: int): guild = self.client.get_guild(guild_id) @@ -527,7 +554,7 @@ class Owner(commands.Cog): await ctx.send(embed=embed) - @commands.command() + @commands.hybrid_command() @commands.is_owner() async def servertour(self, ctx, time_in_seconds: int, member: discord.Member): guild = ctx.guild @@ -594,7 +621,7 @@ class Owner(commands.Cog): - @commands.group() + @commands.hybrid_group() @commands.check(is_owner_or_staff) @blacklist_check() @ignore_check() @@ -657,7 +684,7 @@ class Owner(commands.Cog): await ctx.send(embed=embed) - @commands.command(name="forcepurgebots", + @commands.hybrid_command(name="forcepurgebots", aliases=["fpb"], help="Clear recently bot messages in channel (Bot owner only)") @commands.cooldown(1, 3, commands.BucketType.user) @@ -673,7 +700,7 @@ class Owner(commands.Cog): await do_removal(ctx, search, predicate) - @commands.command(name="forcepurgeuser", + @commands.hybrid_command(name="forcepurgeuser", aliases=["fpu"], help="Clear recent messages of a user in channel (Bot owner only)") @commands.cooldown(1, 3, commands.BucketType.user) diff --git a/bot/cogs/commands/owner2.py b/bot/cogs/commands/owner2.py index 2649c80..2635fdc 100644 --- a/bot/cogs/commands/owner2.py +++ b/bot/cogs/commands/owner2.py @@ -25,14 +25,14 @@ class Global(commands.Cog): self.local_frozen_nicks = {} self.client.frozen_nicknames = {} - @commands.group(name="global", invoke_without_command=True) + @commands.hybrid_group(name="global", invoke_without_command=True) @commands.is_owner() async def global_command(self, ctx: commands.Context): if ctx.subcommand_passed is None: await ctx.send_help(ctx.command) ctx.command.reset_cooldown(ctx) - @commands.command(name="GB",help="Bans the user from all mutual guilds.") + @commands.hybrid_command(name="GB",help="Bans the user from all mutual guilds.") @commands.is_owner() async def global_ban(self, ctx: commands.Context, user: discord.User, reason: str = "Severe violations of Discord's terms of service."): mutual_guilds = [guild for guild in self.client.guilds if guild.get_member(user.id)] @@ -563,7 +563,7 @@ class Global(commands.Cog): await ctx.send(f"✅ | Nickname freezing stopped for {user.name}.") - @commands.command(name="freezenick", help="Freezes a member's nickname in the current server.") + @commands.hybrid_command(name="freezenick", help="Freezes a member's nickname in the current server.") @commands.has_permissions(manage_nicknames=True) async def freeze_nickname(self, ctx: commands.Context, member: Member, *, nickname: str): guild_id = ctx.guild.id @@ -597,7 +597,7 @@ class Global(commands.Cog): self.client.loop.create_task(monitor_nickname()) - @commands.command(name="unfreezenick", help="Unfreezes a member's nickname in the current server.") + @commands.hybrid_command(name="unfreezenick", help="Unfreezes a member's nickname in the current server.") @commands.has_permissions(manage_nicknames=True) async def unfreeze_nickname(self, ctx: commands.Context, member: Member): guild_id = ctx.guild.id diff --git a/bot/cogs/commands/qr.py b/bot/cogs/commands/qr.py index 6b0f82c..5b36fec 100644 --- a/bot/cogs/commands/qr.py +++ b/bot/cogs/commands/qr.py @@ -19,7 +19,7 @@ class QR(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.command( + @commands.hybrid_command( name="qr", aliases=["qrcode"], help="Sends a QR code image.", diff --git a/bot/cogs/commands/slots.py b/bot/cogs/commands/slots.py index 526bbdd..4ef4a38 100644 --- a/bot/cogs/commands/slots.py +++ b/bot/cogs/commands/slots.py @@ -25,7 +25,7 @@ class Slots(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.command(aliases=['slot']) + @commands.hybrid_command(aliases=['slot']) @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) diff --git a/bot/cogs/commands/status.py b/bot/cogs/commands/status.py index 3428753..7c9b6d9 100644 --- a/bot/cogs/commands/status.py +++ b/bot/cogs/commands/status.py @@ -23,7 +23,7 @@ class Status(commands.Cog): def __init__(self, bot): self.bot = bot - @commands.command(name="status", help="Shows the status of the user in detail.") + @commands.hybrid_command(name="status", help="Shows the status of the user in detail.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) diff --git a/bot/cogs/commands/stickymessage.py b/bot/cogs/commands/stickymessage.py index 76ae5f0..a8a05df 100644 --- a/bot/cogs/commands/stickymessage.py +++ b/bot/cogs/commands/stickymessage.py @@ -130,7 +130,7 @@ class StickyMessage(commands.Cog): ) await db.commit() - @commands.group(aliases=['sticky', 'sm'], invoke_without_command=True) + @commands.hybrid_group(aliases=['sticky', 'sm'], invoke_without_command=True) @commands.has_permissions(manage_messages=True) async def stickymessage(self, ctx: commands.Context): if ctx.invoked_subcommand is None: diff --git a/bot/cogs/commands/tracking.py b/bot/cogs/commands/tracking.py index 05acb3b..e24ed2e 100644 --- a/bot/cogs/commands/tracking.py +++ b/bot/cogs/commands/tracking.py @@ -136,7 +136,7 @@ class Tracking(commands.Cog): row = await cursor.fetchone() return row[0] if row else 0 - @commands.command(aliases=["inv"]) + @commands.hybrid_command(aliases=["inv"]) async def invites(self, ctx, member: discord.Member = None): member = member or ctx.author await self.ensure_tables(ctx.guild.id) @@ -161,7 +161,7 @@ class Tracking(commands.Cog): ) await ctx.send(view=CV2(f"Invite Log - {member.name}", desc)) - @commands.command(aliases=["addinvs"]) + @commands.hybrid_command(aliases=["addinvs"]) @commands.has_permissions(administrator=True) async def addinvites(self, ctx, member: discord.Member, amount: int): await self.ensure_tables(ctx.guild.id) @@ -171,7 +171,7 @@ class Tracking(commands.Cog): await db.commit() await ctx.send(view=CV2("✅ Success", f"Added **{amount}** invites to {member.mention}.")) - @commands.command(aliases=["setinvs"]) + @commands.hybrid_command(aliases=["setinvs"]) @commands.has_permissions(administrator=True) async def setinvites(self, ctx, member: discord.Member, amount: int): await self.ensure_tables(ctx.guild.id) @@ -180,7 +180,7 @@ class Tracking(commands.Cog): await db.commit() await ctx.send(view=CV2("✅ Success", f"Set invites of {member.mention} to **{amount}**.")) - @commands.command(aliases=["resetinvs"]) + @commands.hybrid_command(aliases=["resetinvs"]) @commands.has_permissions(administrator=True) async def resetinvites(self, ctx, member: discord.Member): await self.ensure_tables(ctx.guild.id) @@ -189,7 +189,7 @@ class Tracking(commands.Cog): await db.commit() await ctx.send(view=CV2("✅ Success", f"Reset invites of {member.mention}.")) - @commands.command(aliases=["invlb"]) + @commands.hybrid_command(aliases=["invlb"]) async def invitesleaderboard(self, ctx): await self.ensure_tables(ctx.guild.id) async with aiosqlite.connect(INVITE_DB) as db: @@ -208,7 +208,7 @@ class Tracking(commands.Cog): await ctx.send(view=CV2("📊 Invite Leaderboard", leaderboard)) - @commands.command(aliases=["invlog"]) + @commands.hybrid_command(aliases=["invlog"]) @commands.has_permissions(administrator=True) async def invitelogging(self, ctx, channel: discord.TextChannel): await self.ensure_tables(ctx.guild.id) diff --git a/bot/cogs/commands/vanityroles.py b/bot/cogs/commands/vanityroles.py index 8413d9a..89ffdac 100644 --- a/bot/cogs/commands/vanityroles.py +++ b/bot/cogs/commands/vanityroles.py @@ -49,7 +49,7 @@ class VanityRoles(commands.Cog): await db.execute("ALTER TABLE vanity_roles ADD COLUMN current_status TEXT") await db.commit() - @commands.group(name="vanityroles", invoke_without_command=True) + @commands.hybrid_group(name="vanityroles", invoke_without_command=True) @blacklist_check() @ignore_check() async def vanityroles(self, ctx): diff --git a/bot/cogs/commands/voice.py b/bot/cogs/commands/voice.py index 59342e5..e6fcba3 100644 --- a/bot/cogs/commands/voice.py +++ b/bot/cogs/commands/voice.py @@ -29,7 +29,7 @@ class Voice(commands.Cog): self.bot = bot self.color = 0xFF0000 - @commands.group(name="voice", invoke_without_command=True, aliases=['vc']) + @commands.hybrid_group(name="voice", invoke_without_command=True, aliases=['vc']) @blacklist_check() @ignore_check() async def vc(self, ctx: commands.Context): diff --git a/bot/cogs/commands/youtube.py b/bot/cogs/commands/youtube.py index b9ed240..b279aa0 100644 --- a/bot/cogs/commands/youtube.py +++ b/bot/cogs/commands/youtube.py @@ -22,7 +22,7 @@ class Youtube(commands.Cog): self.bot = bot - @commands.command(name='yt', aliases=['youtube']) + @commands.hybrid_command(name='yt', aliases=['youtube']) async def search_youtube(self, ctx, *, search_query): query_string = urllib.parse.urlencode({'search_query': search_query}) html_content = urllib.request.urlopen('http://www.youtube.com/results?' + query_string) diff --git a/bot/cogs/moderation/message.py b/bot/cogs/moderation/message.py index 9625b8e..c80ef1c 100644 --- a/bot/cogs/moderation/message.py +++ b/bot/cogs/moderation/message.py @@ -89,7 +89,7 @@ class Message(commands.Cog): self.color = 0xFF0000 - @commands.group(invoke_without_command=True, aliases=["purge"], help="Clears the messages") + @commands.hybrid_group(invoke_without_command=True, aliases=["purge"], help="Clears the messages") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -232,7 +232,7 @@ class Message(commands.Cog): - @commands.command(name="purgebots", + @commands.hybrid_command(name="purgebots", aliases=["cleanup", "pb", "clearbot", "clearbots"], help="Clear recently bot messages in channel") @blacklist_check() @@ -250,7 +250,7 @@ class Message(commands.Cog): await do_removal(ctx, search, predicate) - @commands.command(name="purgeuser", + @commands.hybrid_command(name="purgeuser", aliases=["pu", "cu", "clearuser"], help="Clear recent messages of a user in channel") @blacklist_check() diff --git a/bot/cogs/moderation/moderation.py b/bot/cogs/moderation/moderation.py index 5b6bbd1..361126f 100644 --- a/bot/cogs/moderation/moderation.py +++ b/bot/cogs/moderation/moderation.py @@ -115,7 +115,7 @@ class Moderation(commands.Cog): - @commands.command() + @commands.hybrid_command() @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -793,7 +793,7 @@ class Moderation(commands.Cog): - @commands.command(aliases=["deletesticker", "removesticker"], description="Delete the sticker from the server") + @commands.hybrid_command(aliases=["deletesticker", "removesticker"], description="Delete the sticker from the server") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -815,7 +815,7 @@ class Moderation(commands.Cog): await ctx.reply("Failed to delete the sticker") - @commands.command(aliases=["deleteemoji", "removeemoji"], description="Deletes the emoji from the server") + @commands.hybrid_command(aliases=["deleteemoji", "removeemoji"], description="Deletes the emoji from the server") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) @@ -864,7 +864,7 @@ class Moderation(commands.Cog): - @commands.command(description="Changes the icon for the role.") + @commands.hybrid_command(description="Changes the icon for the role.") @blacklist_check() @ignore_check() @commands.cooldown(1, 3, commands.BucketType.user) diff --git a/bot/cogs/moderation/role.py b/bot/cogs/moderation/role.py index 0ae4e3a..389fe8b 100644 --- a/bot/cogs/moderation/role.py +++ b/bot/cogs/moderation/role.py @@ -56,7 +56,7 @@ class Role(commands.Cog): self.color = 0xFF0000 - @commands.group(name="role",invoke_without_command=True) + @commands.hybrid_group(name="role",invoke_without_command=True) @blacklist_check() @ignore_check() @commands.cooldown(1, 5, commands.BucketType.user) @@ -598,7 +598,7 @@ class Role(commands.Cog): - @commands.group(name="removerole",invoke_without_command=True, + @commands.hybrid_group(name="removerole",invoke_without_command=True, aliases=['rrole'], help="remove a role from all members .") @blacklist_check() diff --git a/bot/cogs/moderation/snipe.py b/bot/cogs/moderation/snipe.py index f1b9d61..337446c 100644 --- a/bot/cogs/moderation/snipe.py +++ b/bot/cogs/moderation/snipe.py @@ -36,7 +36,7 @@ class Snipe(commands.Cog): 'deleted_at': datetime.utcnow() } - @commands.command(name='snipe', help="Shows the most recently deleted message in the channel.") + @commands.hybrid_command(name='snipe', help="Shows the most recently deleted message in the channel.") @commands.has_permissions(manage_messages=True) async def snipe(self, ctx): # Check if there is a sniped message for the current channel diff --git a/bot/cogs/moderation/topcheck.py b/bot/cogs/moderation/topcheck.py index 5edfc3b..526145e 100644 --- a/bot/cogs/moderation/topcheck.py +++ b/bot/cogs/moderation/topcheck.py @@ -53,7 +53,7 @@ class TopCheck(commands.Cog): await db.execute("UPDATE topcheck SET enabled = 0 WHERE guild_id = ?", (guild_id,)) await db.commit() - @commands.group( + @commands.hybrid_group( name="topcheck", help="Manage topcheck settings for the server.", invoke_without_command=True) diff --git a/bot/core/axiom.py b/bot/core/axiom.py index c69c7e0..a499972 100644 --- a/bot/core/axiom.py +++ b/bot/core/axiom.py @@ -48,11 +48,10 @@ class Axiom(commands.AutoShardedBot): owner_ids=OWNER_IDS, allowed_mentions=discord.AllowedMentions( everyone=False, replied_user=False, roles=False), - sync_commands_debug=True, - sync_commands=True, shard_count=1) self.status_index = 0 self.status_list = [] + self._slash_synced = False async def setup_hook(self): await setup_db() diff --git a/bot/utils/config.py b/bot/utils/config.py index 41c82cd..e82d6fd 100644 --- a/bot/utils/config.py +++ b/bot/utils/config.py @@ -77,6 +77,9 @@ def _parse_ids(env_key: str) -> list[int]: OWNER_IDS: list[int] = _parse_ids("OWNER_IDS") OWNER_IDS_STR: list[str] = [str(i) for i in OWNER_IDS] +# Discord application / OAuth client id (same as dashboard DISCORD_CLIENT_ID) +DISCORD_CLIENT_ID = os.getenv("DISCORD_CLIENT_ID", "").strip() + # Aliases kept for backwards compatibility with files that import these names BOT_OWNER_IDS = OWNER_IDS BOT_OWNER_IDS_STR = OWNER_IDS_STR diff --git a/bot/utils/slash_sync.py b/bot/utils/slash_sync.py new file mode 100644 index 0000000..3b169f1 --- /dev/null +++ b/bot/utils/slash_sync.py @@ -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