Skip to content

Commit adba4f6

Browse files
lorenzo132khakers
authored andcommitted
fix: plain replies deletion and edits (modmail-dev#3416)
* support to edit and delete plain reply messages * fix linting * fix: not rely on mod_color as originally was made. This will avoid crashes when the mod_color get changed. * fix: typeerror / refactor * fix linting * silent unneeded noise
1 parent d7c3543 commit adba4f6

3 files changed

Lines changed: 116 additions & 101 deletions

File tree

bot.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,6 +1918,8 @@ async def on_message_delete(self, message):
19181918
"DM message not found.",
19191919
"Malformed thread message.",
19201920
"Thread message not found.",
1921+
"Linked DM message not found.",
1922+
"Thread message is an internal message, not a note.",
19211923
}:
19221924
logger.debug("Failed to find linked message to delete: %s", e)
19231925
embed = discord.Embed(description="Failed to delete message.", color=self.error_color)

cogs/modmail.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,11 +1729,11 @@ async def edit(self, ctx, message_id: Optional[int] = None, *, message: str):
17291729

17301730
try:
17311731
await thread.edit_message(message_id, message)
1732-
except ValueError:
1732+
except ValueError as e:
17331733
return await ctx.send(
17341734
embed=discord.Embed(
17351735
title="Failed",
1736-
description="Cannot find a message to edit. Plain messages are not supported.",
1736+
description=str(e),
17371737
color=self.bot.error_color,
17381738
)
17391739
)
@@ -2223,7 +2223,7 @@ async def delete(self, ctx, message_id: int = None):
22232223
return await ctx.send(
22242224
embed=discord.Embed(
22252225
title="Failed",
2226-
description="Cannot find a message to delete. Plain messages are not supported.",
2226+
description=str(e),
22272227
color=self.bot.error_color,
22282228
)
22292229
)

core/thread.py

Lines changed: 111 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,117 +1354,118 @@ async def find_linked_messages(
13541354
message1: discord.Message = None,
13551355
note: bool = True,
13561356
) -> typing.Tuple[discord.Message, typing.List[typing.Optional[discord.Message]]]:
1357-
if message1 is not None:
1358-
if note:
1359-
# For notes, don't require author.url; rely on footer/author.name markers
1360-
if not message1.embeds or message1.author != self.bot.user:
1361-
logger.warning(
1362-
f"Malformed note for deletion: embeds={bool(message1.embeds)}, author={message1.author}"
1363-
)
1364-
raise ValueError("Malformed note message.")
1357+
if message1 is None:
1358+
if message_id is not None:
1359+
try:
1360+
message1 = await self.channel.fetch_message(message_id)
1361+
except discord.NotFound:
1362+
logger.warning(f"Message ID {message_id} not found in channel history.")
1363+
raise ValueError("Thread message not found.")
13651364
else:
1366-
if (
1367-
not message1.embeds
1368-
or not message1.embeds[0].author.url
1369-
or message1.author != self.bot.user
1370-
):
1371-
logger.debug(
1372-
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}"
1373-
)
1374-
# Keep original error string to avoid extra failure embeds in on_message_delete
1375-
raise ValueError("Malformed thread message.")
1365+
# No ID provided - find last message sent by bot
1366+
async for msg in self.channel.history():
1367+
if msg.author != self.bot.user:
1368+
continue
1369+
if not msg.embeds:
1370+
continue
13761371

1377-
elif message_id is not None:
1378-
try:
1379-
message1 = await self.channel.fetch_message(message_id)
1380-
except discord.NotFound:
1381-
logger.warning(f"Message ID {message_id} not found in channel history.")
1382-
raise ValueError("Thread message not found.")
1372+
is_valid_candidate = False
1373+
if (
1374+
msg.embeds[0].footer
1375+
and msg.embeds[0].footer.text
1376+
and msg.embeds[0].footer.text.startswith("[PLAIN]")
1377+
):
1378+
is_valid_candidate = True
1379+
elif msg.embeds[0].author.url and msg.embeds[0].author.url.split("#")[-1].isdigit():
1380+
is_valid_candidate = True
1381+
1382+
if is_valid_candidate:
1383+
message1 = msg
1384+
break
13831385

1384-
if note:
1385-
# Try to treat as note/persistent note first
1386-
if message1.embeds and message1.author == self.bot.user:
1387-
footer_text = (message1.embeds[0].footer and message1.embeds[0].footer.text) or ""
1388-
author_name = getattr(message1.embeds[0].author, "name", "") or ""
1389-
is_note = (
1390-
"internal note" in footer_text.lower()
1391-
or "persistent internal note" in footer_text.lower()
1392-
or author_name.startswith("📝 Note")
1393-
or author_name.startswith("📝 Persistent Note")
1394-
)
1395-
if is_note:
1396-
# Notes have no linked DM counterpart; keep None sentinel
1397-
return message1, None
1398-
# else: fall through to relay checks below
1399-
1400-
# Non-note path (regular relayed messages): require author.url and colors
1401-
if not (
1402-
message1.embeds
1403-
and message1.embeds[0].author.url
1404-
and message1.embeds[0].color
1405-
and message1.author == self.bot.user
1406-
):
1407-
logger.warning(
1408-
f"Message {message_id} is not a valid modmail relay message. embeds={bool(message1.embeds)}, author_url={getattr(message1.embeds[0], 'author', None) and message1.embeds[0].author.url}, color={getattr(message1.embeds[0], 'color', None)}, author={message1.author}"
1409-
)
1410-
raise ValueError("Thread message not found.")
1386+
if message1 is None:
1387+
raise ValueError("No editable thread message found.")
1388+
1389+
is_note = False
1390+
if message1.embeds and message1.author == self.bot.user:
1391+
footer_text = (message1.embeds[0].footer and message1.embeds[0].footer.text) or ""
1392+
author_name = getattr(message1.embeds[0].author, "name", "") or ""
1393+
is_note = (
1394+
"internal note" in footer_text.lower()
1395+
or "persistent internal note" in footer_text.lower()
1396+
or author_name.startswith("📝 Note")
1397+
or author_name.startswith("📝 Persistent Note")
1398+
)
14111399

1412-
if message1.embeds[0].footer and "Internal Message" in message1.embeds[0].footer.text:
1413-
if not note:
1414-
logger.warning(
1415-
f"Message {message_id} is an internal message, but note deletion not requested."
1416-
)
1417-
raise ValueError("Thread message is an internal message, not a note.")
1418-
# Internal bot-only message treated similarly; keep None sentinel
1419-
return message1, None
1400+
if note and is_note:
1401+
return message1, None
14201402

1421-
if message1.embeds[0].color.value != self.bot.mod_color and not (
1422-
either_direction and message1.embeds[0].color.value == self.bot.recipient_color
1423-
):
1424-
logger.warning("Message color does not match mod/recipient colors.")
1425-
raise ValueError("Thread message not found.")
1426-
else:
1427-
async for message1 in self.channel.history():
1428-
if (
1429-
message1.embeds
1430-
and message1.embeds[0].author.url
1431-
and message1.embeds[0].color
1432-
and (
1433-
message1.embeds[0].color.value == self.bot.mod_color
1434-
or (either_direction and message1.embeds[0].color.value == self.bot.recipient_color)
1435-
)
1436-
and message1.embeds[0].author.url.split("#")[-1].isdigit()
1437-
and message1.author == self.bot.user
1438-
):
1439-
break
1440-
else:
1403+
if not note and is_note:
1404+
raise ValueError("Thread message is an internal message, not a note.")
1405+
1406+
if is_note:
1407+
return message1, None
1408+
1409+
is_plain = False
1410+
if message1.embeds and message1.embeds[0].footer and message1.embeds[0].footer.text:
1411+
if message1.embeds[0].footer.text.startswith("[PLAIN]"):
1412+
is_plain = True
1413+
1414+
if not is_plain:
1415+
# Relaxed mod_color check: only ensure author is bot and has url (which implies it's a relay)
1416+
# We rely on author.url existing for Joint ID
1417+
if not (message1.embeds and message1.embeds[0].author.url and message1.author == self.bot.user):
14411418
raise ValueError("Thread message not found.")
14421419

1443-
try:
1444-
joint_id = int(message1.embeds[0].author.url.split("#")[-1])
1445-
except ValueError:
1446-
raise ValueError("Malformed thread message.")
1420+
try:
1421+
joint_id = int(message1.embeds[0].author.url.split("#")[-1])
1422+
except (ValueError, AttributeError, IndexError):
1423+
raise ValueError("Malformed thread message.")
1424+
else:
1425+
joint_id = None
1426+
mod_tag = message1.embeds[0].footer.text.replace("[PLAIN]", "", 1).strip()
1427+
author_name = message1.embeds[0].author.name
1428+
desc = message1.embeds[0].description or ""
1429+
prefix = f"**{mod_tag} " if mod_tag else "**"
1430+
plain_content_expected = f"{prefix}{author_name}:** {desc}"
1431+
creation_time = message1.created_at
14471432

14481433
messages = [message1]
1449-
for user in self.recipients:
1450-
async for msg in user.history():
1451-
if either_direction:
1452-
if msg.id == joint_id:
1453-
return message1, msg
14541434

1455-
if not (msg.embeds and msg.embeds[0].author.url):
1456-
continue
1457-
try:
1458-
if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id:
1435+
if is_plain:
1436+
for user in self.recipients:
1437+
async for msg in user.history(limit=50, around=creation_time):
1438+
if abs((msg.created_at - creation_time).total_seconds()) > 15:
1439+
continue
1440+
if msg.author != self.bot.user:
1441+
continue
1442+
if msg.embeds:
1443+
continue
1444+
1445+
if msg.content == plain_content_expected:
14591446
messages.append(msg)
14601447
break
1461-
except ValueError:
1462-
continue
1448+
else:
1449+
for user in self.recipients:
1450+
async for msg in user.history():
1451+
if either_direction:
1452+
if msg.id == joint_id:
1453+
messages.append(msg)
1454+
break
1455+
1456+
if not (msg.embeds and msg.embeds[0].author.url):
1457+
continue
1458+
try:
1459+
if int(msg.embeds[0].author.url.split("#")[-1]) == joint_id:
1460+
messages.append(msg)
1461+
break
1462+
except (ValueError, IndexError, AttributeError):
1463+
continue
14631464

14641465
if len(messages) > 1:
14651466
return messages
14661467

1467-
raise ValueError("DM message not found.")
1468+
raise ValueError("Linked DM message not found.")
14681469

14691470
async def edit_message(self, message_id: typing.Optional[int], message: str) -> None:
14701471
try:
@@ -1476,6 +1477,10 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) ->
14761477
embed1 = message1.embeds[0]
14771478
embed1.description = message
14781479

1480+
is_plain = False
1481+
if embed1.footer and embed1.footer.text and embed1.footer.text.startswith("[PLAIN]"):
1482+
is_plain = True
1483+
14791484
tasks = [
14801485
self.bot.api.edit_message(message1.id, message),
14811486
message1.edit(embed=embed1),
@@ -1485,9 +1490,17 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) ->
14851490
else:
14861491
for m2 in message2:
14871492
if m2 is not None:
1488-
embed2 = m2.embeds[0]
1489-
embed2.description = message
1490-
tasks += [m2.edit(embed=embed2)]
1493+
if is_plain:
1494+
# Reconstruct the plain message format to preserve matching capability
1495+
mod_tag = embed1.footer.text.replace("[PLAIN]", "", 1).strip()
1496+
author_name = embed1.author.name
1497+
prefix = f"**{mod_tag} " if mod_tag else "**"
1498+
new_content = f"{prefix}{author_name}:** {message}"
1499+
tasks += [m2.edit(content=new_content)]
1500+
else:
1501+
embed2 = m2.embeds[0]
1502+
embed2.description = message
1503+
tasks += [m2.edit(embed=embed2)]
14911504

14921505
await asyncio.gather(*tasks)
14931506

0 commit comments

Comments
 (0)