From 79a577f3e4e4acca8f390eaf66b51c81a428e091 Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Sat, 13 Dec 2025 15:32:46 +0100 Subject: [PATCH 1/4] feat: bulk override perms for commands --- cogs/utility.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/cogs/utility.py b/cogs/utility.py index d14aa97baa..316790de6c 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1382,7 +1382,7 @@ def _parse_level(name): @permissions.command(name="override") @checks.has_permissions(PermissionLevel.OWNER) - async def permissions_override(self, ctx, command_name: str.lower, *, level_name: str): + async def permissions_override(self, ctx, command_name: str.lower, *, level_name: str = None): """ Change a permission level for a specific command. @@ -1396,8 +1396,16 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name - `{prefix}perms remove override reply` - `{prefix}perms remove override plugin enabled` + You can also override multiple commands at once using: + - `{prefix}perms override bulk` + You can retrieve a single or all command level override(s), see`{prefix}help permissions get`. """ + if command_name == "bulk": + return await self._bulk_override_flow(ctx) + + if level_name is None: + raise commands.MissingRequiredArgument(DummyParam("level_name")) command = self.bot.get_command(command_name) if command is None: @@ -1432,6 +1440,126 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name ) return await ctx.send(embed=embed) + async def _bulk_override_flow(self, ctx): + await ctx.send( + "Please list the commands you want to override. " + "You can list multiple commands separated by spaces or newlines.\n" + "Example: `ban, kick, mod`." + ) + + try: + msg = await self.bot.wait_for( + "message", + check=lambda m: m.author == ctx.author and m.channel == ctx.channel, + timeout=120.0, + ) + except asyncio.TimeoutError: + return await ctx.send("Timed out.") + + raw_commands = msg.content.replace(",", " ").replace("\n", " ").split(" ") + # Filter empty strings from split + raw_commands = [c for c in raw_commands if c.strip()] + + found_commands = [] + invalid_commands = [] + + for cmd_name in raw_commands: + cmd = self.bot.get_command(cmd_name) + if cmd: + found_commands.append(cmd) + else: + invalid_commands.append(cmd_name) + + if invalid_commands: + embed = discord.Embed( + title="Invalid Commands Found", + description=f"The following commands were not found:\n`{', '.join(invalid_commands)}`\n\n" + "Do you want to continue with the valid commands? (y/n)", + color=self.bot.error_color, + ) + await ctx.send(embed=embed) + try: + msg = await self.bot.wait_for( + "message", + check=lambda m: m.author == ctx.author and m.channel == ctx.channel, + timeout=60.0, + ) + if msg.content.lower() not in ("y", "yes"): + return await ctx.send("Aborted.") + except asyncio.TimeoutError: + return await ctx.send("Timed out.") + + if not found_commands: + return await ctx.send("No valid commands provided. Aborting.") + + # Expand subcommands + final_commands = set() + + def add_command_recursive(cmd): + final_commands.add(cmd) + if hasattr(cmd, "commands"): + for sub in cmd.commands: + add_command_recursive(sub) + + for cmd in found_commands: + add_command_recursive(cmd) + + await ctx.send( + f"Found {len(final_commands)} commands (including subcommands).\n" + "What permission level should these commands be set to? (e.g. `Owner`, `Admin`, `Moderator`, `Supporter`, `User`)" + ) + + try: + msg = await self.bot.wait_for( + "message", + check=lambda m: m.author == ctx.author and m.channel == ctx.channel, + timeout=60.0, + ) + except asyncio.TimeoutError: + return await ctx.send("Timed out.") + + level_name = msg.content + level = self._parse_level(level_name) + if level == PermissionLevel.INVALID: + return await ctx.send(f"Invalid permission level: `{level_name}`. Aborting.") + + # Confirmation + command_list_str = ", ".join(f"`{c.qualified_name}`" for c in sorted(final_commands, key=lambda x: x.qualified_name)) + + # Truncate if too long for embed description + if len(command_list_str) > 2000: + command_list_str = command_list_str[:1997] + "..." + + embed = discord.Embed( + title="Confirm Bulk Override", + description=f"**Level:** {level.name}\n\n**Commands:**\n{command_list_str}\n\n" + "Type `confirm` to apply these changes or `cancel` to abort.", + color=self.bot.main_color, + ) + await ctx.send(embed=embed) + + try: + msg = await self.bot.wait_for( + "message", + check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content.lower() in ("confirm", "cancel"), + timeout=30.0, + ) + except asyncio.TimeoutError: + return await ctx.send("Timed out. No changes applied.") + + if msg.content.lower() == "cancel": + return await ctx.send("Aborted.") + + # Apply changes + count = 0 + for cmd in final_commands: + self.bot.config["override_command_level"][cmd.qualified_name] = level.name + count += 1 + + await self.bot.config.update() + + await ctx.send(f"Successfully updated permissions for {count} commands.") + @permissions.command(name="add", usage="[command/level] [name] [user/role]") @checks.has_permissions(PermissionLevel.OWNER) async def permissions_add( From fef2b99162b6fe7749f52f8153eaf87b452852b3 Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Sat, 13 Dec 2025 15:34:38 +0100 Subject: [PATCH 2/4] fix linting --- cogs/utility.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 316790de6c..22b1382483 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1524,8 +1524,10 @@ def add_command_recursive(cmd): return await ctx.send(f"Invalid permission level: `{level_name}`. Aborting.") # Confirmation - command_list_str = ", ".join(f"`{c.qualified_name}`" for c in sorted(final_commands, key=lambda x: x.qualified_name)) - + command_list_str = ", ".join( + f"`{c.qualified_name}`" for c in sorted(final_commands, key=lambda x: x.qualified_name) + ) + # Truncate if too long for embed description if len(command_list_str) > 2000: command_list_str = command_list_str[:1997] + "..." @@ -1541,7 +1543,9 @@ def add_command_recursive(cmd): try: msg = await self.bot.wait_for( "message", - check=lambda m: m.author == ctx.author and m.channel == ctx.channel and m.content.lower() in ("confirm", "cancel"), + check=lambda m: m.author == ctx.author + and m.channel == ctx.channel + and m.content.lower() in ("confirm", "cancel"), timeout=30.0, ) except asyncio.TimeoutError: @@ -1555,9 +1559,9 @@ def add_command_recursive(cmd): for cmd in final_commands: self.bot.config["override_command_level"][cmd.qualified_name] = level.name count += 1 - + await self.bot.config.update() - + await ctx.send(f"Successfully updated permissions for {count} commands.") @permissions.command(name="add", usage="[command/level] [name] [user/role]") From 75ec6b6a605426283910e976d5ad4c6a3167a9d8 Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Sun, 14 Dec 2025 03:41:06 +0100 Subject: [PATCH 3/4] change reflection upon review --- cogs/utility.py | 83 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index 22b1382483..d0e0e5389a 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1441,11 +1441,16 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name return await ctx.send(embed=embed) async def _bulk_override_flow(self, ctx): - await ctx.send( - "Please list the commands you want to override. " - "You can list multiple commands separated by spaces or newlines.\n" - "Example: `ban, kick, mod`." + embed = discord.Embed( + title="Bulk Override", + description=( + "Please list the commands you want to override. " + "You can list multiple commands separated by spaces or newlines.\n" + "Example: `reply, block, unblock`." + ), + color=self.bot.main_color, ) + await ctx.send(embed=embed) try: msg = await self.bot.wait_for( @@ -1453,13 +1458,21 @@ async def _bulk_override_flow(self, ctx): check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=120.0, ) + except asyncio.TimeoutError: - return await ctx.send("Timed out.") + return await ctx.send(embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color)) raw_commands = msg.content.replace(",", " ").replace("\n", " ").split(" ") # Filter empty strings from split raw_commands = [c for c in raw_commands if c.strip()] + if self.bot.prefix: + # Strip prefix from commands if present + raw_commands = [ + c[len(self.bot.prefix) :] if c.startswith(self.bot.prefix) else c + for c in raw_commands + ] + found_commands = [] invalid_commands = [] @@ -1485,12 +1498,22 @@ async def _bulk_override_flow(self, ctx): timeout=60.0, ) if msg.content.lower() not in ("y", "yes"): - return await ctx.send("Aborted.") + return await ctx.send( + embed=discord.Embed( + title="Operation Aborted", + description="No changes have been applied.", + color=self.bot.error_color, + ) + ) except asyncio.TimeoutError: - return await ctx.send("Timed out.") + return await ctx.send(embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color)) if not found_commands: - return await ctx.send("No valid commands provided. Aborting.") + return await ctx.send( + embed=discord.Embed( + title="Error", description="No valid commands provided. Aborting.", color=self.bot.error_color + ) + ) # Expand subcommands final_commands = set() @@ -1504,10 +1527,15 @@ def add_command_recursive(cmd): for cmd in found_commands: add_command_recursive(cmd) - await ctx.send( - f"Found {len(final_commands)} commands (including subcommands).\n" - "What permission level should these commands be set to? (e.g. `Owner`, `Admin`, `Moderator`, `Supporter`, `User`)" + embed = discord.Embed( + title="Select Permission Level", + description=( + f"Found {len(final_commands)} commands (including subcommands).\n" + "What permission level should these commands be set to? (e.g. `Owner`, `Admin`, `Moderator`, `Supporter`, `User`)" + ), + color=self.bot.main_color, ) + await ctx.send(embed=embed) try: msg = await self.bot.wait_for( @@ -1515,13 +1543,20 @@ def add_command_recursive(cmd): check=lambda m: m.author == ctx.author and m.channel == ctx.channel, timeout=60.0, ) + except asyncio.TimeoutError: - return await ctx.send("Timed out.") + return await ctx.send(embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color)) level_name = msg.content level = self._parse_level(level_name) if level == PermissionLevel.INVALID: - return await ctx.send(f"Invalid permission level: `{level_name}`. Aborting.") + return await ctx.send( + embed=discord.Embed( + title="Error", + description=f"Invalid permission level: `{level_name}`. Aborting.", + color=self.bot.error_color, + ) + ) # Confirmation command_list_str = ", ".join( @@ -1549,10 +1584,20 @@ def add_command_recursive(cmd): timeout=30.0, ) except asyncio.TimeoutError: - return await ctx.send("Timed out. No changes applied.") + return await ctx.send( + embed=discord.Embed( + title="Error", description="Timed out. No changes applied.", color=self.bot.error_color + ) + ) if msg.content.lower() == "cancel": - return await ctx.send("Aborted.") + return await ctx.send( + embed=discord.Embed( + title="Operation Aborted", + description="No changes have been applied.", + color=self.bot.error_color, + ) + ) # Apply changes count = 0 @@ -1562,7 +1607,13 @@ def add_command_recursive(cmd): await self.bot.config.update() - await ctx.send(f"Successfully updated permissions for {count} commands.") + await ctx.send( + embed=discord.Embed( + title="Success", + description=f"Successfully updated permissions for {count} commands.", + color=self.bot.main_color, + ) + ) @permissions.command(name="add", usage="[command/level] [name] [user/role]") @checks.has_permissions(PermissionLevel.OWNER) From 45e6f789d5a87b77941d8318c559e0df82fe472b Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Sun, 14 Dec 2025 03:44:32 +0100 Subject: [PATCH 4/4] linting --- cogs/utility.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index d0e0e5389a..0f9230f9a5 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -1460,7 +1460,9 @@ async def _bulk_override_flow(self, ctx): ) except asyncio.TimeoutError: - return await ctx.send(embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color)) + return await ctx.send( + embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color) + ) raw_commands = msg.content.replace(",", " ").replace("\n", " ").split(" ") # Filter empty strings from split @@ -1469,8 +1471,7 @@ async def _bulk_override_flow(self, ctx): if self.bot.prefix: # Strip prefix from commands if present raw_commands = [ - c[len(self.bot.prefix) :] if c.startswith(self.bot.prefix) else c - for c in raw_commands + c[len(self.bot.prefix) :] if c.startswith(self.bot.prefix) else c for c in raw_commands ] found_commands = [] @@ -1506,12 +1507,16 @@ async def _bulk_override_flow(self, ctx): ) ) except asyncio.TimeoutError: - return await ctx.send(embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color)) + return await ctx.send( + embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color) + ) if not found_commands: return await ctx.send( embed=discord.Embed( - title="Error", description="No valid commands provided. Aborting.", color=self.bot.error_color + title="Error", + description="No valid commands provided. Aborting.", + color=self.bot.error_color, ) ) @@ -1545,7 +1550,9 @@ def add_command_recursive(cmd): ) except asyncio.TimeoutError: - return await ctx.send(embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color)) + return await ctx.send( + embed=discord.Embed(title="Error", description="Timed out.", color=self.bot.error_color) + ) level_name = msg.content level = self._parse_level(level_name)