-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix: plain replies deletion and edits #3416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f33b06c
20af225
e620a03
4e38eaa
420ece0
2992eb0
2758afe
9e549a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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), | ||||||||||||
| color=self.bot.error_color, | ||||||||||||
| ) | ||||||||||||
| ) | ||||||||||||
|
|
@@ -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), | ||||||||||||
|
||||||||||||
| 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." | |
| ), |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1334,117 +1334,118 @@ async def find_linked_messages( | |||||||||||||||
| message1: discord.Message = None, | ||||||||||||||||
| note: bool = True, | ||||||||||||||||
| ) -> typing.Tuple[discord.Message, typing.List[typing.Optional[discord.Message]]]: | ||||||||||||||||
| if message1 is not None: | ||||||||||||||||
| if note: | ||||||||||||||||
| # For notes, don't require author.url; rely on footer/author.name markers | ||||||||||||||||
| if not message1.embeds or message1.author != self.bot.user: | ||||||||||||||||
| logger.warning( | ||||||||||||||||
| f"Malformed note for deletion: embeds={bool(message1.embeds)}, author={message1.author}" | ||||||||||||||||
| ) | ||||||||||||||||
| raise ValueError("Malformed note message.") | ||||||||||||||||
| if message1 is None: | ||||||||||||||||
| if message_id is not None: | ||||||||||||||||
| try: | ||||||||||||||||
| message1 = await self.channel.fetch_message(message_id) | ||||||||||||||||
| except discord.NotFound: | ||||||||||||||||
| logger.warning(f"Message ID {message_id} not found in channel history.") | ||||||||||||||||
| raise ValueError("Thread message not found.") | ||||||||||||||||
| else: | ||||||||||||||||
| if ( | ||||||||||||||||
| not message1.embeds | ||||||||||||||||
| 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.") | ||||||||||||||||
| # No ID provided - find last message sent by bot | ||||||||||||||||
| async for msg in self.channel.history(): | ||||||||||||||||
| if msg.author != self.bot.user: | ||||||||||||||||
| continue | ||||||||||||||||
| if not msg.embeds: | ||||||||||||||||
| continue | ||||||||||||||||
|
|
||||||||||||||||
| elif message_id is not None: | ||||||||||||||||
| try: | ||||||||||||||||
| message1 = await self.channel.fetch_message(message_id) | ||||||||||||||||
| except discord.NotFound: | ||||||||||||||||
| logger.warning(f"Message ID {message_id} not found in channel history.") | ||||||||||||||||
| raise ValueError("Thread message not found.") | ||||||||||||||||
| is_valid_candidate = False | ||||||||||||||||
| if ( | ||||||||||||||||
| msg.embeds[0].footer | ||||||||||||||||
| and msg.embeds[0].footer.text | ||||||||||||||||
| and msg.embeds[0].footer.text.startswith("[PLAIN]") | ||||||||||||||||
| ): | ||||||||||||||||
| is_valid_candidate = True | ||||||||||||||||
| elif msg.embeds[0].author.url and msg.embeds[0].author.url.split("#")[-1].isdigit(): | ||||||||||||||||
| is_valid_candidate = True | ||||||||||||||||
|
Comment on lines
+1359
to
+1360
|
||||||||||||||||
| elif msg.embeds[0].author.url and msg.embeds[0].author.url.split("#")[-1].isdigit(): | |
| is_valid_candidate = True | |
| else: | |
| author = msg.embeds[0].author | |
| if author and author.url and author.url.split("#")[-1].isdigit(): | |
| is_valid_candidate = True |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for checking if a message is a note uses both footer and author fields. However, if message1.embeds[0] doesn't have an author attribute, the getattr call on line 1372 will return an empty string, but this doesn't protect against AttributeError when checking author_name.startswith. While getattr provides a default, the issue is that if embeds[0].author is None, getattr returns the default correctly. However, consider adding an explicit check for embeds[0].author existence for clarity.
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return type annotation indicates the method returns a Tuple containing a discord.Message and a List of Optional discord.Messages. However, when returning for notes (line 1381 and 1387), the method returns (message1, None) where None is not a List. This violates the type contract. The return should be (message1, []) or the type annotation should be updated to allow None as the second element.
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is unreachable.
| if is_note: | |
| return message1, None |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The check for msg.embeds[0].author.url should first verify that msg.embeds[0].author exists. If the embed has no author attribute or author is None, accessing msg.embeds[0].author.url will raise an AttributeError.
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable joint_id is not used.
| joint_id = None |
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the issue in find_linked_messages, accessing embed1.author.name without first checking if embed1.author exists can cause an AttributeError. Verify that embed1.author is not None before accessing its name attribute.
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
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.
| 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
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code constructs the expected plain content to match against, but this logic is brittle. If the mod_tag contains special characters or if there are any formatting differences in how the message was originally constructed versus how it's being reconstructed here, the match will fail. Consider a more robust matching strategy, such as checking for key components separately or using fuzzy matching for the content.
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
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
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return statement at line 1446 returns messages (a list), not a tuple as specified by the return type annotation. According to the type annotation, the method should return typing.Tuple[discord.Message, typing.List[typing.Optional[discord.Message]]], but here it's returning just the list. This should be return (messages[0], messages[1:]) to match the type signature and maintain consistency with how the result is unpacked in callers like line 1452.
Copilot
AI
Jan 16, 2026
There was a problem hiding this comment.
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.
| 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
AI
Jan 16, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When reconstructing the plain message content for editing, the code directly accesses embed1.author.name without checking if embed1.author exists. If the embed doesn't have an author field, this will raise an AttributeError. Add a check to ensure embed1.author is not None before accessing its name attribute.
| author_name = embed1.author.name | |
| author_name = embed1.author.name if embed1.author and embed1.author.name else "" |
There was a problem hiding this comment.
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.