@@ -1408,7 +1408,7 @@ def _parse_level(name):
14081408
14091409 @permissions .command (name = "override" )
14101410 @checks .has_permissions (PermissionLevel .OWNER )
1411- async def permissions_override (self , ctx , command_name : str .lower , * , level_name : str ):
1411+ async def permissions_override (self , ctx , command_name : str .lower , * , level_name : str = None ):
14121412 """
14131413 Change a permission level for a specific command.
14141414
@@ -1422,8 +1422,16 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name
14221422 - `{prefix}perms remove override reply`
14231423 - `{prefix}perms remove override plugin enabled`
14241424
1425+ You can also override multiple commands at once using:
1426+ - `{prefix}perms override bulk`
1427+
14251428 You can retrieve a single or all command level override(s), see`{prefix}help permissions get`.
14261429 """
1430+ if command_name == "bulk" :
1431+ return await self ._bulk_override_flow (ctx )
1432+
1433+ if level_name is None :
1434+ raise commands .MissingRequiredArgument (DummyParam ("level_name" ))
14271435
14281436 command = self .bot .get_command (command_name )
14291437 if command is None :
@@ -1458,6 +1466,221 @@ async def permissions_override(self, ctx, command_name: str.lower, *, level_name
14581466 )
14591467 return await ctx .send (embed = embed )
14601468
1469+ async def _bulk_override_flow (self , ctx ):
1470+ message = None
1471+ embed = discord .Embed (
1472+ title = "Bulk Override" ,
1473+ description = (
1474+ "Please list the commands you want to override. "
1475+ "You can list multiple commands separated by spaces or newlines.\n "
1476+ "Example: `reply, block, unblock`.\n "
1477+ ),
1478+ color = self .bot .main_color ,
1479+ )
1480+ await ctx .send (embed = embed )
1481+
1482+ try :
1483+ msg = await self .bot .wait_for (
1484+ "message" ,
1485+ check = lambda m : m .author == ctx .author and m .channel == ctx .channel ,
1486+ timeout = 120.0 ,
1487+ )
1488+
1489+ except asyncio .TimeoutError :
1490+ return await ctx .send (
1491+ embed = discord .Embed (title = "Error" , description = "Timed out." , color = self .bot .error_color )
1492+ )
1493+
1494+ raw_commands = msg .content .replace ("," , " " ).replace ("\n " , " " ).split (" " )
1495+ # Filter empty strings from split
1496+ raw_commands = [c for c in raw_commands if c .strip ()]
1497+
1498+ # Strip prefix from commands if present
1499+ prefixes = [self .bot .prefix , f"<@{ self .bot .user .id } >" , f"<@!{ self .bot .user .id } >" ]
1500+ if self .bot .prefix :
1501+ for i , cmd in enumerate (raw_commands ):
1502+ for p in prefixes :
1503+ if cmd .startswith (p ):
1504+ raw_commands [i ] = cmd [len (p ) :]
1505+ break
1506+
1507+ # Filter empty strings again after stripping prefixes
1508+ raw_commands = [c for c in raw_commands if c .strip ()]
1509+
1510+ found_commands = []
1511+ invalid_commands = []
1512+ seen_commands = set ()
1513+ duplicate_count = 0
1514+
1515+ # Commands that should not be bulk-updated for safety reasons
1516+ blocked_commands = {"eval" }
1517+
1518+ for cmd_name in raw_commands :
1519+ cmd = self .bot .get_command (cmd_name )
1520+ if cmd :
1521+ if cmd .qualified_name in blocked_commands :
1522+ invalid_commands .append (cmd_name )
1523+ elif cmd .qualified_name in seen_commands :
1524+ duplicate_count += 1
1525+ else :
1526+ seen_commands .add (cmd .qualified_name )
1527+ found_commands .append (cmd )
1528+ else :
1529+ invalid_commands .append (cmd_name )
1530+
1531+ if invalid_commands or duplicate_count > 0 :
1532+ description = ""
1533+ if invalid_commands :
1534+ description += f"The following commands were not found or are blocked:\n `{ ', ' .join (invalid_commands )} `\n \n "
1535+ if duplicate_count > 0 :
1536+ description += (
1537+ f"Ignoring { duplicate_count } duplicate command{ 's' if duplicate_count > 1 else '' } .\n \n "
1538+ )
1539+ if found_commands :
1540+ found_list = ", " .join (c .qualified_name for c in found_commands )
1541+ found_list = utils .return_or_truncate (found_list , 1000 )
1542+ description += f"The following commands **were** found:\n `{ found_list } `\n \n "
1543+
1544+ description += "Do you want to continue with the valid commands?"
1545+
1546+ embed = discord .Embed (
1547+ title = "Invalid Commands Found" ,
1548+ description = description ,
1549+ color = self .bot .error_color ,
1550+ )
1551+ view = discord .ui .View ()
1552+ view .add_item (utils .AcceptButton (custom_id = "continue" , emoji = "✅" ))
1553+ view .add_item (utils .DenyButton (custom_id = "abort" , emoji = "❌" ))
1554+
1555+ message = await ctx .send (embed = embed , view = view )
1556+ timed_out = await view .wait ()
1557+
1558+ if timed_out or not view .value :
1559+ return await message .edit (
1560+ embed = discord .Embed (
1561+ title = "Operation Aborted" ,
1562+ description = "No changes have been applied." ,
1563+ color = self .bot .error_color ,
1564+ ),
1565+ view = None ,
1566+ )
1567+
1568+ if not found_commands :
1569+ return await ctx .send (
1570+ embed = discord .Embed (
1571+ title = "Error" ,
1572+ description = "No valid commands provided. Aborting." ,
1573+ color = self .bot .error_color ,
1574+ )
1575+ )
1576+
1577+ # Expand subcommands
1578+ final_commands = set ()
1579+
1580+ def add_command_recursive (cmd ):
1581+ final_commands .add (cmd )
1582+ if hasattr (cmd , "commands" ):
1583+ for sub in cmd .commands :
1584+ add_command_recursive (sub )
1585+
1586+ for cmd in found_commands :
1587+ add_command_recursive (cmd )
1588+
1589+ embed = discord .Embed (
1590+ title = "Select Permission Level" ,
1591+ description = (
1592+ f"Found { len (final_commands )} commands (including subcommands).\n "
1593+ "What permission level should these commands be set to?"
1594+ ),
1595+ color = self .bot .main_color ,
1596+ )
1597+
1598+ class LevelSelect (discord .ui .Select ):
1599+ def __init__ (self ):
1600+ options = [
1601+ discord .SelectOption (label = "Owner" , value = "OWNER" ),
1602+ discord .SelectOption (label = "Administrator" , value = "ADMINISTRATOR" ),
1603+ discord .SelectOption (label = "Moderator" , value = "MODERATOR" ),
1604+ discord .SelectOption (label = "Supporter" , value = "SUPPORTER" ),
1605+ discord .SelectOption (label = "Regular" , value = "REGULAR" ),
1606+ ]
1607+ super ().__init__ (placeholder = "Select permission level..." , options = options )
1608+
1609+ async def callback (self , interaction : discord .Interaction ):
1610+ self .view .value = self .values [0 ]
1611+ self .view .stop ()
1612+ await interaction .response .defer ()
1613+
1614+ view = discord .ui .View ()
1615+ view .add_item (LevelSelect ())
1616+
1617+ if message :
1618+ await message .edit (embed = embed , view = view )
1619+ else :
1620+ message = await ctx .send (embed = embed , view = view )
1621+ timed_out = await view .wait ()
1622+
1623+ if timed_out or view .value is None :
1624+ return await message .edit (
1625+ embed = discord .Embed (title = "Error" , description = "Timed out." , color = self .bot .error_color ),
1626+ view = None ,
1627+ )
1628+
1629+ level_name = view .value
1630+ level = self ._parse_level (level_name )
1631+
1632+ # Confirmation
1633+ command_list_str = ", " .join (
1634+ f"`{ c .qualified_name } `" for c in sorted (final_commands , key = lambda x : x .qualified_name )
1635+ )
1636+
1637+ command_list_str = utils .return_or_truncate (command_list_str , 2048 )
1638+
1639+ embed = discord .Embed (
1640+ title = "Confirm Bulk Override" ,
1641+ description = f"**Level:** { level .name } \n \n **Commands:**\n { command_list_str } " ,
1642+ color = self .bot .main_color ,
1643+ )
1644+
1645+ view = discord .ui .View ()
1646+ view .add_item (utils .AcceptButton (custom_id = "confirm" , emoji = "✅" ))
1647+ view .add_item (utils .DenyButton (custom_id = "cancel" , emoji = "❌" ))
1648+
1649+ await message .edit (embed = embed , view = view )
1650+ timed_out = await view .wait ()
1651+
1652+ if timed_out or not view .value :
1653+ return await message .edit (
1654+ embed = discord .Embed (
1655+ title = "Operation Aborted" ,
1656+ description = "No changes have been applied." ,
1657+ color = self .bot .error_color ,
1658+ ),
1659+ view = None ,
1660+ )
1661+
1662+ # Apply changes
1663+ for cmd in final_commands :
1664+ self .bot .config ["override_command_level" ][cmd .qualified_name ] = level .name
1665+
1666+ await self .bot .config .update ()
1667+
1668+ logger .info (
1669+ "Bulk override: set permission level %s for %d commands: %s" ,
1670+ level .name ,
1671+ len (final_commands ),
1672+ ", " .join (cmd .qualified_name for cmd in final_commands ),
1673+ )
1674+
1675+ await message .edit (
1676+ embed = discord .Embed (
1677+ title = "Success" ,
1678+ description = f"Successfully updated permissions for { len (final_commands )} commands." ,
1679+ color = self .bot .main_color ,
1680+ ),
1681+ view = None ,
1682+ )
1683+
14611684 @permissions .command (name = "add" , usage = "[command/level] [name] [user/role]" )
14621685 @checks .has_permissions (PermissionLevel .OWNER )
14631686 async def permissions_add (
0 commit comments