Skip to content

Commit dff4a62

Browse files
authored
Merge branch 'development' into users/lorenzo/breaking-threadmenu-fix
Signed-off-by: lorenzo132 <50767078+lorenzo132@users.noreply.github.com>
2 parents 90371cc + 70c8b56 commit dff4a62

5 files changed

Lines changed: 326 additions & 46 deletions

File tree

bot.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,8 @@ def __init__(self, original_message, forwarded_content):
10181018
self.author = original_message.author
10191019
self.content = forwarded_content
10201020
self.attachments = []
1021+
for snap in getattr(original_message, "message_snapshots", []):
1022+
self.attachments.extend(getattr(snap, "attachments", []))
10211023
self.stickers = []
10221024
self.created_at = original_message.created_at
10231025
self.embeds = []

cogs/utility.py

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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(

core/clients.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pymongo.errors import ConfigurationError
1313

1414
from core.models import InvalidConfigError, getLogger
15+
from core.utils import extract_forwarded_content
1516

1617
logger = getLogger(__name__)
1718

core/thread.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
ConfirmThreadCreationView,
3636
DummyParam,
3737
extract_forwarded_content,
38+
extract_forwarded_attachments,
3839
)
3940

4041
logger = getLogger(__name__)
@@ -207,7 +208,10 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None):
207208
"messages": [
208209
{
209210
"author_id": m.author.id,
210-
"content": m.content,
211+
"content": (
212+
(m.content or "")
213+
+ (("\n" + extract_forwarded_content(m)) if extract_forwarded_content(m) else "")
214+
).strip(),
211215
"attachments": [a.url for a in m.attachments],
212216
"embeds": [e.to_dict() for e in m.embeds],
213217
"created_at": m.created_at.isoformat(),
@@ -1997,6 +2001,11 @@ async def send(
19972001

19982002
ext = [(a.url, a.filename, False) for a in message.attachments]
19992003

2004+
# Add forwarded message attachments
2005+
forwarded_attachments = extract_forwarded_attachments(message)
2006+
for url, filename in forwarded_attachments:
2007+
ext.append((url, filename, False))
2008+
20002009
images = []
20012010
attachments = []
20022011
for attachment in ext:
@@ -3390,7 +3399,7 @@ async def callback(self, interaction: discord.Interaction):
33903399
ctxs = []
33913400
for al in normalize_alias(alias):
33923401
view_ = StringView(self.outer_thread.bot.prefix + al)
3393-
synthetic = DummyMessage(copy.copy(message))
3402+
synthetic = DummyMessage(copy.copy(self.outer_thread._genesis_message))
33943403
try:
33953404
synthetic.author = (
33963405
self.outer_thread.bot.modmail_guild.me or self.outer_thread.bot.user

0 commit comments

Comments
 (0)