From f33b06c39d70a3eaefbc31cc994dfb284e3e46af Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Sat, 13 Dec 2025 14:59:50 +0100 Subject: [PATCH 1/2] support to edit and delete plain reply messages --- cogs/modmail.py | 6 +-- core/thread.py | 109 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 89 insertions(+), 26 deletions(-) diff --git a/cogs/modmail.py b/cogs/modmail.py index 0e39da920c..cbab46bcb0 100644 --- a/cogs/modmail.py +++ b/cogs/modmail.py @@ -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), color=self.bot.error_color, ) ) diff --git a/core/thread.py b/core/thread.py index 09263b197d..7d0960c223 100644 --- a/core/thread.py +++ b/core/thread.py @@ -224,16 +224,12 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): "author_name": ( getattr(m.embeds[0].author, "name", "").split(" (")[0] if m.embeds and m.embeds[0].author and m.author == self.bot.user - else getattr(m.author, "name", None) - if m.author != self.bot.user - else None + else getattr(m.author, "name", None) if m.author != self.bot.user else None ), "author_avatar": ( getattr(m.embeds[0].author, "icon_url", None) if m.embeds and m.embeds[0].author and m.author == self.bot.user - else m.author.display_avatar.url - if m.author != self.bot.user - else None + else m.author.display_avatar.url if m.author != self.bot.user else None ), } async for m in channel.history(limit=None, oldest_first=True) @@ -1345,11 +1341,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: @@ -1374,8 +1376,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 @@ -1395,8 +1401,10 @@ 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.") @@ -1404,19 +1412,62 @@ async def find_linked_messages( 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.") + 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: + 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: @@ -1453,6 +1504,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), @@ -1462,9 +1517,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) From 20af22526e32811931e19f62ee221cb1a7fa687d Mon Sep 17 00:00:00 2001 From: lorenzo132 Date: Sat, 13 Dec 2025 15:03:16 +0100 Subject: [PATCH 2/2] fix linting --- core/thread.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/thread.py b/core/thread.py index 7d0960c223..913cd2a38e 100644 --- a/core/thread.py +++ b/core/thread.py @@ -224,12 +224,16 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None): "author_name": ( getattr(m.embeds[0].author, "name", "").split(" (")[0] if m.embeds and m.embeds[0].author and m.author == self.bot.user - else getattr(m.author, "name", None) if m.author != self.bot.user else None + else getattr(m.author, "name", None) + if m.author != self.bot.user + else None ), "author_avatar": ( getattr(m.embeds[0].author, "icon_url", None) if m.embeds and m.embeds[0].author and m.author == self.bot.user - else m.author.display_avatar.url if m.author != self.bot.user else None + else m.author.display_avatar.url + if m.author != self.bot.user + else None ), } async for m in channel.history(limit=None, oldest_first=True)