Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
130 changes: 74 additions & 56 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,39 @@ 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 is_plain:
creation_time = message1.created_at

mod_tag = message1.embeds[0].footer.text.replace("[PLAIN]", "", 1).strip()
author_name = message1.embeds[0].author.name
desc = message1.embeds[0].description or ""
prefix = f"**{mod_tag} " if mod_tag else "**"
plain_content_expected = f"{prefix}{author_name}:** {desc}"

messages = [message1]
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:
continue
if msg.author != self.bot.user:
continue
if msg.embeds:
continue

if msg.content == plain_content_expected:
messages.append(msg)
break

if len(messages) > 1:
return messages
raise ValueError("Linked Plain DM message not found.")

if not is_plain and not (
message1.embeds
and message1.embeds[0].author.url
and message1.embeds[0].color
Expand All @@ -1387,61 +1424,38 @@ async def find_linked_messages(
raise ValueError("Thread message not found.")

if message1.embeds[0].footer and "Internal Message" in message1.embeds[0].footer.text:
if not note:
logger.warning(
f"Message {message_id} is an internal message, but note deletion not requested."
)
raise ValueError("Thread message is an internal message, not a note.")
logger.warning(
f"Message {message_id} is an internal message, but note deletion not requested."
)
raise ValueError("Thread message is an internal message, not a note.")
# 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
):
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)
)
and message1.embeds[0].author.url.split("#")[-1].isdigit()
and message1.author == self.bot.user
):
break
else:
raise ValueError("Thread message not found.")

try:
joint_id = int(message1.embeds[0].author.url.split("#")[-1])
except ValueError:
raise ValueError("Malformed thread message.")

messages = [message1]
for user in self.recipients:
async for msg in user.history():
if either_direction:
if msg.id == joint_id:
return message1, msg
try:
joint_id = int(message1.embeds[0].author.url.split("#")[-1])
except ValueError:
raise ValueError("Malformed thread message.")

messages = [message1]
for user in self.recipients:
async for msg in user.history():
if either_direction:
if msg.id == joint_id:
return message1, msg

if not (msg.embeds and msg.embeds[0].author.url):
continue
try:
if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id:
messages.append(msg)
break
except ValueError:
continue
if not (msg.embeds and msg.embeds[0].author.url):
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 code checks msg.embeds[0].author.url without first verifying that msg.embeds[0].author exists. If an embed doesn't have an author field, this will raise an AttributeError. Add a check to ensure msg.embeds[0].author is not None before accessing its url attribute.

Copilot uses AI. Check for mistakes.
continue
try:
if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id:
messages.append(msg)
break
except ValueError:
continue

if len(messages) > 1:
return messages
if len(messages) > 1:
return messages

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 error "Linked DM message not found." is raised when len(messages) <= 1, but this doesn't distinguish between the case where no linked messages were found versus when we're dealing with a note that doesn't have linked messages. For notes that return early with (message1, None), this is fine, but for regular messages, this error assumes that a linked message should always exist. Consider whether this is the intended behavior for all message types, particularly for plain messages where finding the linked DM might legitimately fail.

Suggested change
if is_plain:
# For plain messages, it's acceptable that no linked DM is found;
# in that case, just return the original message.
return messages

Copilot uses AI. Check for mistakes.
raise ValueError("DM message not found.")
raise ValueError("DM message not found.")

async def edit_message(self, message_id: typing.Optional[int], message: str) -> None:
try:
Expand All @@ -1453,6 +1467,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 Down
Loading