From fc77f0a3c24d8e2bd97f3ddd05978c51dfb5db67 Mon Sep 17 00:00:00 2001 From: TheOnlyMace <0815cracky@gmail.com> Date: Tue, 21 Jul 2026 22:41:41 +0200 Subject: [PATCH] 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. --- .env.docker.example | 5 + bot/.env.example | 7 +- bot/Axiom.py | 32 +++-- bot/cogs/commands/Birthday.py | 10 +- bot/cogs/commands/Games.py | 2 +- bot/cogs/commands/Invc.py | 2 +- bot/cogs/commands/autoreact.py | 2 +- bot/cogs/commands/autoresponder.py | 2 +- bot/cogs/commands/autorole.py | 2 +- bot/cogs/commands/blackjack.py | 2 +- bot/cogs/commands/blacklist.py | 2 +- bot/cogs/commands/block.py | 2 +- bot/cogs/commands/calc.py | 2 +- bot/cogs/commands/counting.py | 2 +- bot/cogs/commands/dms.py | 2 +- bot/cogs/commands/emergency.py | 6 +- bot/cogs/commands/encryption.py | 6 +- bot/cogs/commands/extra.py | 10 +- bot/cogs/commands/fastgreet.py | 6 +- bot/cogs/commands/fun.py | 36 +++--- bot/cogs/commands/general.py | 8 +- bot/cogs/commands/ignore.py | 2 +- bot/cogs/commands/image.py | 8 +- bot/cogs/commands/j2c.py | 4 +- bot/cogs/commands/jail.py | 6 +- bot/cogs/commands/joindm.py | 2 +- bot/cogs/commands/leveling.py | 2 +- bot/cogs/commands/messages.py | 2 +- bot/cogs/commands/msgpack.py | 6 +- bot/cogs/commands/music.py | 32 ++--- bot/cogs/commands/nitro.py | 2 +- bot/cogs/commands/notify.py | 2 +- bot/cogs/commands/np.py | 2 +- bot/cogs/commands/owner.py | 71 +++++++---- bot/cogs/commands/owner2.py | 8 +- bot/cogs/commands/qr.py | 2 +- bot/cogs/commands/slots.py | 2 +- bot/cogs/commands/status.py | 2 +- bot/cogs/commands/stickymessage.py | 2 +- bot/cogs/commands/tracking.py | 12 +- bot/cogs/commands/vanityroles.py | 2 +- bot/cogs/commands/voice.py | 2 +- bot/cogs/commands/youtube.py | 2 +- bot/cogs/moderation/message.py | 6 +- bot/cogs/moderation/moderation.py | 8 +- bot/cogs/moderation/role.py | 4 +- bot/cogs/moderation/snipe.py | 2 +- bot/cogs/moderation/topcheck.py | 2 +- bot/core/axiom.py | 3 +- bot/utils/config.py | 3 + bot/utils/slash_sync.py | 184 +++++++++++++++++++++++++++++ 51 files changed, 382 insertions(+), 153 deletions(-) create mode 100644 bot/utils/slash_sync.py 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