Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cogs/modmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -1724,11 +1724,11 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str):

try:
await thread.edit_message(message_id, message)
except ValueError:
except ValueError as e:
return await ctx.send(
embed=discord.Embed(
title="Failed",
description="Cannot find a message to edit. Plain messages are not supported.",
description=str(e),
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning the raw exception message str(e) directly to users in error messages can expose internal implementation details or confusing technical messages. Consider mapping specific ValueError messages to more user-friendly error descriptions, or ensure that all ValueError messages raised in the thread module are written with end-users in mind.

Copilot uses AI. Check for mistakes.
color=self.bot.error_color,
)
)
Expand Down Expand Up @@ -2274,7 +2274,7 @@ async def delete(self, ctx, message_id: int = None):
return await ctx.send(
embed=discord.Embed(
title="Failed",
description="Cannot find a message to delete. Plain messages are not supported.",
description=str(e),
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning the raw exception message str(e) directly to users in error messages can expose internal implementation details or confusing technical messages. Consider mapping specific ValueError messages to more user-friendly error descriptions, or ensure that all ValueError messages raised in the thread module are written with end-users in mind.

Suggested change
description=str(e),
description=(
"I couldn't delete that message. It may not exist, may not have been "
"sent via Modmail, or cannot be deleted."
),

Copilot uses AI. Check for mistakes.
color=self.bot.error_color,
)
)
Expand Down
101 changes: 84 additions & 17 deletions core/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -1345,11 +1345,17 @@ async def find_linked_messages(
or not message1.embeds[0].author.url
or message1.author != self.bot.user
):
logger.debug(
f"Malformed thread message for deletion: embeds={bool(message1.embeds)}, author_url={getattr(message1.embeds[0], 'author', None) and message1.embeds[0].author.url}, author={message1.author}"
)
# Keep original error string to avoid extra failure embeds in on_message_delete
raise ValueError("Malformed thread message.")
is_plain = False
if message1.embeds and message1.embeds[0].footer and message1.embeds[0].footer.text:
if message1.embeds[0].footer.text.startswith("[PLAIN]"):
is_plain = True

if not is_plain:
logger.debug(
f"Malformed thread message for deletion: embeds={bool(message1.embeds)}, author_url={getattr(message1.embeds[0], 'author', None) and message1.embeds[0].author.url}, author={message1.author}"
)
# Keep original error string to avoid extra failure embeds in on_message_delete
raise ValueError("Malformed thread message.")

elif message_id is not None:
try:
Expand All @@ -1374,8 +1380,12 @@ async def find_linked_messages(
return message1, None
# else: fall through to relay checks below

# Non-note path (regular relayed messages): require author.url and colors
if not (
is_plain = False
if message1.embeds and message1.embeds[0].footer and message1.embeds[0].footer.text:
if message1.embeds[0].footer.text.startswith("[PLAIN]"):
is_plain = True

if not is_plain and not (
message1.embeds
and message1.embeds[0].author.url
and message1.embeds[0].color
Expand All @@ -1395,28 +1405,73 @@ async def find_linked_messages(
# Internal bot-only message treated similarly; keep None sentinel
return message1, None

if message1.embeds[0].color.value != self.bot.mod_color and not (
either_direction and message1.embeds[0].color.value == self.bot.recipient_color
if (
not is_plain
and message1.embeds[0].color.value != self.bot.mod_color
and not (either_direction and message1.embeds[0].color.value == self.bot.recipient_color)
):
logger.warning("Message color does not match mod/recipient colors.")
raise ValueError("Thread message not found.")
else:
async for message1 in self.channel.history():
if (
message1.embeds
and message1.embeds[0].author.url
and message1.embeds[0].color
and (
message1.embeds[0].color.value == self.bot.mod_color
or (either_direction and message1.embeds[0].color.value == self.bot.recipient_color)
(
message1.embeds[0].author.url
and message1.embeds[0].color
and (
message1.embeds[0].color.value == self.bot.mod_color
or (
either_direction
and message1.embeds[0].color.value == self.bot.recipient_color
)
)
and message1.embeds[0].author.url.split("#")[-1].isdigit()
)
or (
message1.embeds[0].footer
and message1.embeds[0].footer.text
and message1.embeds[0].footer.text.startswith("[PLAIN]")
)
)
and message1.embeds[0].author.url.split("#")[-1].isdigit()
and message1.author == self.bot.user
):
break
else:
raise ValueError("Thread message not found.")

Comment on lines +1386 to +1388
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This statement is unreachable.

Suggested change
if is_note:
return message1, None

Copilot uses AI. Check for mistakes.
is_plain = False
if message1.embeds and message1.embeds[0].footer and message1.embeds[0].footer.text:
if message1.embeds[0].footer.text.startswith("[PLAIN]"):
is_plain = True

if is_plain:
messages = [message1]
creation_time = message1.created_at

target_content = message1.embeds[0].description

for user in self.recipients:
async for msg in user.history(limit=50, around=creation_time):
if abs((msg.created_at - creation_time).total_seconds()) > 15:
Comment on lines +1416 to +1418
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The time window check of 15 seconds may be too restrictive for matching plain messages. If there's any network delay, message processing delay, or if the bot is under load, the DM message could be created slightly more than 15 seconds after the thread message, causing the match to fail. Consider increasing this tolerance to at least 30-60 seconds to account for potential delays.

Suggested change
for user in self.recipients:
async for msg in user.history(limit=50, around=creation_time):
if abs((msg.created_at - creation_time).total_seconds()) > 15:
tolerance_seconds = 60
for user in self.recipients:
async for msg in user.history(limit=50, around=creation_time):
if abs((msg.created_at - creation_time).total_seconds()) > tolerance_seconds:

Copilot uses AI. Check for mistakes.
continue

if msg.author != self.bot.user:
continue

if msg.embeds:
continue

if target_content and target_content in msg.content:
messages.append(msg)
break

if len(messages) > 1:
return messages

raise ValueError("Linked Plain DM message not found.")

try:
joint_id = int(message1.embeds[0].author.url.split("#")[-1])
except ValueError:
Expand Down Expand Up @@ -1453,6 +1508,10 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) ->
embed1 = message1.embeds[0]
embed1.description = message

is_plain = False
if embed1.footer and embed1.footer.text and embed1.footer.text.startswith("[PLAIN]"):
is_plain = True

tasks = [
self.bot.api.edit_message(message1.id, message),
message1.edit(embed=embed1),
Expand All @@ -1462,9 +1521,17 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) ->
else:
for m2 in message2:
if m2 is not None:
embed2 = m2.embeds[0]
embed2.description = message
tasks += [m2.edit(embed=embed2)]
if is_plain:
if ":** " in m2.content:
prefix = m2.content.split(":** ", 1)[0] + ":** "
new_content = f"{prefix}{message}"
tasks += [m2.edit(content=new_content)]
else:
tasks += [m2.edit(content=message)]
else:
embed2 = m2.embeds[0]
embed2.description = message
tasks += [m2.edit(embed=embed2)]

await asyncio.gather(*tasks)

Expand Down
Loading