@@ -1338,117 +1338,118 @@ async def find_linked_messages(
13381338 message1 : discord .Message = None ,
13391339 note : bool = True ,
13401340 ) -> typing .Tuple [discord .Message , typing .List [typing .Optional [discord .Message ]]]:
1341- if message1 is not None :
1342- if note :
1343- # For notes, don't require author.url; rely on footer/author.name markers
1344- if not message1 .embeds or message1 .author != self .bot .user :
1345- logger .warning (
1346- f"Malformed note for deletion: embeds={ bool (message1 .embeds )} , author={ message1 .author } "
1347- )
1348- raise ValueError ("Malformed note message." )
1341+ if message1 is None :
1342+ if message_id is not None :
1343+ try :
1344+ message1 = await self .channel .fetch_message (message_id )
1345+ except discord .NotFound :
1346+ logger .warning (f"Message ID { message_id } not found in channel history." )
1347+ raise ValueError ("Thread message not found." )
13491348 else :
1350- if (
1351- not message1 .embeds
1352- or not message1 .embeds [0 ].author .url
1353- or message1 .author != self .bot .user
1354- ):
1355- logger .debug (
1356- 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 } "
1357- )
1358- # Keep original error string to avoid extra failure embeds in on_message_delete
1359- raise ValueError ("Malformed thread message." )
1349+ # No ID provided - find last message sent by bot
1350+ async for msg in self .channel .history ():
1351+ if msg .author != self .bot .user :
1352+ continue
1353+ if not msg .embeds :
1354+ continue
13601355
1361- elif message_id is not None :
1362- try :
1363- message1 = await self .channel .fetch_message (message_id )
1364- except discord .NotFound :
1365- logger .warning (f"Message ID { message_id } not found in channel history." )
1366- raise ValueError ("Thread message not found." )
1356+ is_valid_candidate = False
1357+ if (
1358+ msg .embeds [0 ].footer
1359+ and msg .embeds [0 ].footer .text
1360+ and msg .embeds [0 ].footer .text .startswith ("[PLAIN]" )
1361+ ):
1362+ is_valid_candidate = True
1363+ elif msg .embeds [0 ].author .url and msg .embeds [0 ].author .url .split ("#" )[- 1 ].isdigit ():
1364+ is_valid_candidate = True
1365+
1366+ if is_valid_candidate :
1367+ message1 = msg
1368+ break
13671369
1368- if note :
1369- # Try to treat as note/persistent note first
1370- if message1 .embeds and message1 .author == self .bot .user :
1371- footer_text = (message1 .embeds [0 ].footer and message1 .embeds [0 ].footer .text ) or ""
1372- author_name = getattr (message1 .embeds [0 ].author , "name" , "" ) or ""
1373- is_note = (
1374- "internal note" in footer_text .lower ()
1375- or "persistent internal note" in footer_text .lower ()
1376- or author_name .startswith ("📝 Note" )
1377- or author_name .startswith ("📝 Persistent Note" )
1378- )
1379- if is_note :
1380- # Notes have no linked DM counterpart; keep None sentinel
1381- return message1 , None
1382- # else: fall through to relay checks below
1383-
1384- # Non-note path (regular relayed messages): require author.url and colors
1385- if not (
1386- message1 .embeds
1387- and message1 .embeds [0 ].author .url
1388- and message1 .embeds [0 ].color
1389- and message1 .author == self .bot .user
1390- ):
1391- logger .warning (
1392- 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 } "
1393- )
1394- raise ValueError ("Thread message not found." )
1370+ if message1 is None :
1371+ raise ValueError ("No editable thread message found." )
1372+
1373+ is_note = False
1374+ if message1 .embeds and message1 .author == self .bot .user :
1375+ footer_text = (message1 .embeds [0 ].footer and message1 .embeds [0 ].footer .text ) or ""
1376+ author_name = getattr (message1 .embeds [0 ].author , "name" , "" ) or ""
1377+ is_note = (
1378+ "internal note" in footer_text .lower ()
1379+ or "persistent internal note" in footer_text .lower ()
1380+ or author_name .startswith ("📝 Note" )
1381+ or author_name .startswith ("📝 Persistent Note" )
1382+ )
13951383
1396- if message1 .embeds [0 ].footer and "Internal Message" in message1 .embeds [0 ].footer .text :
1397- if not note :
1398- logger .warning (
1399- f"Message { message_id } is an internal message, but note deletion not requested."
1400- )
1401- raise ValueError ("Thread message is an internal message, not a note." )
1402- # Internal bot-only message treated similarly; keep None sentinel
1403- return message1 , None
1384+ if note and is_note :
1385+ return message1 , None
14041386
1405- if message1 .embeds [0 ].color .value != self .bot .mod_color and not (
1406- either_direction and message1 .embeds [0 ].color .value == self .bot .recipient_color
1407- ):
1408- logger .warning ("Message color does not match mod/recipient colors." )
1409- raise ValueError ("Thread message not found." )
1410- else :
1411- async for message1 in self .channel .history ():
1412- if (
1413- message1 .embeds
1414- and message1 .embeds [0 ].author .url
1415- and message1 .embeds [0 ].color
1416- and (
1417- message1 .embeds [0 ].color .value == self .bot .mod_color
1418- or (either_direction and message1 .embeds [0 ].color .value == self .bot .recipient_color )
1419- )
1420- and message1 .embeds [0 ].author .url .split ("#" )[- 1 ].isdigit ()
1421- and message1 .author == self .bot .user
1422- ):
1423- break
1424- else :
1387+ if not note and is_note :
1388+ raise ValueError ("Thread message is an internal message, not a note." )
1389+
1390+ if is_note :
1391+ return message1 , None
1392+
1393+ is_plain = False
1394+ if message1 .embeds and message1 .embeds [0 ].footer and message1 .embeds [0 ].footer .text :
1395+ if message1 .embeds [0 ].footer .text .startswith ("[PLAIN]" ):
1396+ is_plain = True
1397+
1398+ if not is_plain :
1399+ # Relaxed mod_color check: only ensure author is bot and has url (which implies it's a relay)
1400+ # We rely on author.url existing for Joint ID
1401+ if not (message1 .embeds and message1 .embeds [0 ].author .url and message1 .author == self .bot .user ):
14251402 raise ValueError ("Thread message not found." )
14261403
1427- try :
1428- joint_id = int (message1 .embeds [0 ].author .url .split ("#" )[- 1 ])
1429- except ValueError :
1430- raise ValueError ("Malformed thread message." )
1404+ try :
1405+ joint_id = int (message1 .embeds [0 ].author .url .split ("#" )[- 1 ])
1406+ except (ValueError , AttributeError , IndexError ):
1407+ raise ValueError ("Malformed thread message." )
1408+ else :
1409+ joint_id = None
1410+ mod_tag = message1 .embeds [0 ].footer .text .replace ("[PLAIN]" , "" , 1 ).strip ()
1411+ author_name = message1 .embeds [0 ].author .name
1412+ desc = message1 .embeds [0 ].description or ""
1413+ prefix = f"**{ mod_tag } " if mod_tag else "**"
1414+ plain_content_expected = f"{ prefix } { author_name } :** { desc } "
1415+ creation_time = message1 .created_at
14311416
14321417 messages = [message1 ]
1433- for user in self .recipients :
1434- async for msg in user .history ():
1435- if either_direction :
1436- if msg .id == joint_id :
1437- return message1 , msg
14381418
1439- if not (msg .embeds and msg .embeds [0 ].author .url ):
1440- continue
1441- try :
1442- if int (msg .embeds [0 ].author .url .split ("#" )[- 1 ]) == joint_id :
1419+ if is_plain :
1420+ for user in self .recipients :
1421+ async for msg in user .history (limit = 50 , around = creation_time ):
1422+ if abs ((msg .created_at - creation_time ).total_seconds ()) > 15 :
1423+ continue
1424+ if msg .author != self .bot .user :
1425+ continue
1426+ if msg .embeds :
1427+ continue
1428+
1429+ if msg .content == plain_content_expected :
14431430 messages .append (msg )
14441431 break
1445- except ValueError :
1446- continue
1432+ else :
1433+ for user in self .recipients :
1434+ async for msg in user .history ():
1435+ if either_direction :
1436+ if msg .id == joint_id :
1437+ messages .append (msg )
1438+ break
1439+
1440+ if not (msg .embeds and msg .embeds [0 ].author .url ):
1441+ continue
1442+ try :
1443+ if int (msg .embeds [0 ].author .url .split ("#" )[- 1 ]) == joint_id :
1444+ messages .append (msg )
1445+ break
1446+ except (ValueError , IndexError , AttributeError ):
1447+ continue
14471448
14481449 if len (messages ) > 1 :
14491450 return messages
14501451
1451- raise ValueError ("DM message not found." )
1452+ raise ValueError ("Linked DM message not found." )
14521453
14531454 async def edit_message (self , message_id : typing .Optional [int ], message : str ) -> None :
14541455 try :
@@ -1460,6 +1461,10 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) ->
14601461 embed1 = message1 .embeds [0 ]
14611462 embed1 .description = message
14621463
1464+ is_plain = False
1465+ if embed1 .footer and embed1 .footer .text and embed1 .footer .text .startswith ("[PLAIN]" ):
1466+ is_plain = True
1467+
14631468 tasks = [
14641469 self .bot .api .edit_message (message1 .id , message ),
14651470 message1 .edit (embed = embed1 ),
@@ -1469,9 +1474,17 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) ->
14691474 else :
14701475 for m2 in message2 :
14711476 if m2 is not None :
1472- embed2 = m2 .embeds [0 ]
1473- embed2 .description = message
1474- tasks += [m2 .edit (embed = embed2 )]
1477+ if is_plain :
1478+ # Reconstruct the plain message format to preserve matching capability
1479+ mod_tag = embed1 .footer .text .replace ("[PLAIN]" , "" , 1 ).strip ()
1480+ author_name = embed1 .author .name
1481+ prefix = f"**{ mod_tag } " if mod_tag else "**"
1482+ new_content = f"{ prefix } { author_name } :** { message } "
1483+ tasks += [m2 .edit (content = new_content )]
1484+ else :
1485+ embed2 = m2 .embeds [0 ]
1486+ embed2 .description = message
1487+ tasks += [m2 .edit (embed = embed2 )]
14751488
14761489 await asyncio .gather (* tasks )
14771490
0 commit comments