@@ -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