@@ -224,16 +224,12 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None):
224224 "author_name" : (
225225 getattr (m .embeds [0 ].author , "name" , "" ).split (" (" )[0 ]
226226 if m .embeds and m .embeds [0 ].author and m .author == self .bot .user
227- else getattr (m .author , "name" , None )
228- if m .author != self .bot .user
229- else None
227+ else getattr (m .author , "name" , None ) if m .author != self .bot .user else None
230228 ),
231229 "author_avatar" : (
232230 getattr (m .embeds [0 ].author , "icon_url" , None )
233231 if m .embeds and m .embeds [0 ].author and m .author == self .bot .user
234- else m .author .display_avatar .url
235- if m .author != self .bot .user
236- else None
232+ else m .author .display_avatar .url if m .author != self .bot .user else None
237233 ),
238234 }
239235 async for m in channel .history (limit = None , oldest_first = True )
@@ -1331,131 +1327,122 @@ async def find_linked_messages(
13311327 message1 : discord .Message = None ,
13321328 note : bool = True ,
13331329 ) -> typing .Tuple [discord .Message , typing .List [typing .Optional [discord .Message ]]]:
1334- if message1 is not None :
1335- if note :
1336- # For notes, don't require author.url; rely on footer/author.name markers
1337- if not message1 .embeds or message1 .author != self .bot .user :
1338- logger .warning (
1339- f"Malformed note for deletion: embeds={ bool (message1 .embeds )} , author={ message1 .author } "
1340- )
1341- raise ValueError ("Malformed note message." )
1330+ if message1 is None :
1331+ if message_id is not None :
1332+ try :
1333+ message1 = await self .channel .fetch_message (message_id )
1334+ except discord .NotFound :
1335+ logger .warning (f"Message ID { message_id } not found in channel history." )
1336+ raise ValueError ("Thread message not found." )
13421337 else :
1343- if (
1344- not message1 .embeds
1345- or not message1 .embeds [0 ].author .url
1346- or message1 .author != self .bot .user
1347- ):
1348- is_plain = False
1349- if message1 .embeds and message1 .embeds [0 ].footer and message1 .embeds [0 ].footer .text :
1350- if message1 .embeds [0 ].footer .text .startswith ("[PLAIN]" ):
1351- is_plain = True
1338+ # No ID provided - find last message sent by bot
1339+ async for msg in self .channel .history ():
1340+ if msg .author != self .bot .user :
1341+ continue
1342+ if not msg .embeds :
1343+ continue
13521344
1353- if not is_plain :
1354- logger .debug (
1355- 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 } "
1356- )
1357- # Keep original error string to avoid extra failure embeds in on_message_delete
1358- raise ValueError ("Malformed thread message." )
1345+ is_valid_candidate = False
1346+ if (
1347+ msg .embeds [0 ].footer
1348+ and msg .embeds [0 ].footer .text
1349+ and msg .embeds [0 ].footer .text .startswith ("[PLAIN]" )
1350+ ):
1351+ is_valid_candidate = True
1352+ elif msg .embeds [0 ].author .url and msg .embeds [0 ].author .url .split ("#" )[- 1 ].isdigit ():
1353+ is_valid_candidate = True
1354+
1355+ if is_valid_candidate :
1356+ message1 = msg
1357+ break
13591358
1360- elif message_id is not None :
1361- try :
1362- message1 = await self .channel .fetch_message (message_id )
1363- except discord .NotFound :
1364- logger .warning (f"Message ID { message_id } not found in channel history." )
1365- raise ValueError ("Thread message not found." )
1359+ if message1 is None :
1360+ raise ValueError ("No editable thread message found." )
1361+
1362+ is_note = False
1363+ if message1 .embeds and message1 .author == self .bot .user :
1364+ footer_text = (message1 .embeds [0 ].footer and message1 .embeds [0 ].footer .text ) or ""
1365+ author_name = getattr (message1 .embeds [0 ].author , "name" , "" ) or ""
1366+ is_note = (
1367+ "internal note" in footer_text .lower ()
1368+ or "persistent internal note" in footer_text .lower ()
1369+ or author_name .startswith ("📝 Note" )
1370+ or author_name .startswith ("📝 Persistent Note" )
1371+ )
13661372
1367- if note :
1368- # Try to treat as note/persistent note first
1369- if message1 .embeds and message1 .author == self .bot .user :
1370- footer_text = (message1 .embeds [0 ].footer and message1 .embeds [0 ].footer .text ) or ""
1371- author_name = getattr (message1 .embeds [0 ].author , "name" , "" ) or ""
1372- is_note = (
1373- "internal note" in footer_text .lower ()
1374- or "persistent internal note" in footer_text .lower ()
1375- or author_name .startswith ("📝 Note" )
1376- or author_name .startswith ("📝 Persistent Note" )
1377- )
1378- if is_note :
1379- # Notes have no linked DM counterpart; keep None sentinel
1380- return message1 , None
1381- # else: fall through to relay checks below
1382-
1383- is_plain = False
1384- if message1 .embeds and message1 .embeds [0 ].footer and message1 .embeds [0 ].footer .text :
1385- if message1 .embeds [0 ].footer .text .startswith ("[PLAIN]" ):
1386- is_plain = True
1387-
1388- if is_plain :
1389- creation_time = message1 .created_at
1390-
1391- mod_tag = message1 .embeds [0 ].footer .text .replace ("[PLAIN]" , "" , 1 ).strip ()
1392- author_name = message1 .embeds [0 ].author .name
1393- desc = message1 .embeds [0 ].description or ""
1394- prefix = f"**{ mod_tag } " if mod_tag else "**"
1395- plain_content_expected = f"{ prefix } { author_name } :** { desc } "
1396-
1397- messages = [message1 ]
1398- for user in self .recipients :
1399- async for msg in user .history (limit = 50 , around = creation_time ):
1400- if abs ((msg .created_at - creation_time ).total_seconds ()) > 15 :
1401- continue
1402- if msg .author != self .bot .user :
1403- continue
1404- if msg .embeds :
1405- continue
1373+ if note and is_note :
1374+ return message1 , None
14061375
1407- if msg .content == plain_content_expected :
1408- messages .append (msg )
1409- break
1410-
1411- if len (messages ) > 1 :
1412- return messages
1413- raise ValueError ("Linked Plain DM message not found." )
1414-
1415- if not is_plain and not (
1416- message1 .embeds
1417- and message1 .embeds [0 ].author .url
1418- and message1 .embeds [0 ].color
1419- and message1 .author == self .bot .user
1420- ):
1421- logger .warning (
1422- 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 } "
1423- )
1424- raise ValueError ("Thread message not found." )
1376+ if not note and is_note :
1377+ logger .warning ("Message is an internal message, but note deletion/edit not requested." )
1378+ raise ValueError ("Thread message is an internal message, not a note." )
14251379
1426- if message1 .embeds [0 ].footer and "Internal Message" in message1 .embeds [0 ].footer .text :
1380+ if is_note :
1381+ return message1 , None
1382+
1383+ is_plain = False
1384+ if message1 .embeds and message1 .embeds [0 ].footer and message1 .embeds [0 ].footer .text :
1385+ if message1 .embeds [0 ].footer .text .startswith ("[PLAIN]" ):
1386+ is_plain = True
1387+
1388+ if not is_plain :
1389+ # Relaxed mod_color check: only ensure author is bot and has url (which implies it's a relay)
1390+ # We rely on author.url existing for Joint ID
1391+ if not (message1 .embeds and message1 .embeds [0 ].author .url and message1 .author == self .bot .user ):
14271392 logger .warning (
1428- f"Message { message_id } is an internal message, but note deletion not requested. "
1393+ f"Message { message1 . id } is not a valid modmail relay message. embeds= { bool ( message1 . embeds ) } , author= { message1 . author } "
14291394 )
1430- raise ValueError ("Thread message is an internal message, not a note." )
1431- # Internal bot-only message treated similarly; keep None sentinel
1432- return message1 , None
1395+ raise ValueError ("Thread message not found." )
14331396
14341397 try :
14351398 joint_id = int (message1 .embeds [0 ].author .url .split ("#" )[- 1 ])
1436- except ValueError :
1399+ except ( ValueError , AttributeError , IndexError ) :
14371400 raise ValueError ("Malformed thread message." )
1438-
1439- messages = [message1 ]
1401+ else :
1402+ joint_id = None
1403+ mod_tag = message1 .embeds [0 ].footer .text .replace ("[PLAIN]" , "" , 1 ).strip ()
1404+ author_name = message1 .embeds [0 ].author .name
1405+ desc = message1 .embeds [0 ].description or ""
1406+ prefix = f"**{ mod_tag } " if mod_tag else "**"
1407+ plain_content_expected = f"{ prefix } { author_name } :** { desc } "
1408+ creation_time = message1 .created_at
1409+
1410+ messages = [message1 ]
1411+
1412+ if is_plain :
1413+ for user in self .recipients :
1414+ async for msg in user .history (limit = 50 , around = creation_time ):
1415+ if abs ((msg .created_at - creation_time ).total_seconds ()) > 15 :
1416+ continue
1417+ if msg .author != self .bot .user :
1418+ continue
1419+ if msg .embeds :
1420+ continue
1421+
1422+ if msg .content == plain_content_expected :
1423+ messages .append (msg )
1424+ break
1425+ else :
14401426 for user in self .recipients :
14411427 async for msg in user .history ():
14421428 if either_direction :
14431429 if msg .id == joint_id :
1444- return message1 , msg
1430+ messages .append (msg )
1431+ break
14451432
14461433 if not (msg .embeds and msg .embeds [0 ].author .url ):
14471434 continue
14481435 try :
14491436 if int (msg .embeds [0 ].author .url .split ("#" )[- 1 ]) == joint_id :
14501437 messages .append (msg )
14511438 break
1452- except ValueError :
1439+ except ( ValueError , IndexError , AttributeError ) :
14531440 continue
14541441
1455- if len (messages ) > 1 :
1456- return messages
1442+ if len (messages ) > 1 :
1443+ return messages
14571444
1458- raise ValueError ("DM message not found." )
1445+ raise ValueError ("Linked DM message not found." )
14591446
14601447 async def edit_message (self , message_id : typing .Optional [int ], message : str ) -> None :
14611448 try :
@@ -1480,9 +1467,17 @@ async def edit_message(self, message_id: typing.Optional[int], message: str) ->
14801467 else :
14811468 for m2 in message2 :
14821469 if m2 is not None :
1483- embed2 = m2 .embeds [0 ]
1484- embed2 .description = message
1485- tasks += [m2 .edit (embed = embed2 )]
1470+ if is_plain :
1471+ # Reconstruct the plain message format to preserve matching capability
1472+ mod_tag = embed1 .footer .text .replace ("[PLAIN]" , "" , 1 ).strip ()
1473+ author_name = embed1 .author .name
1474+ prefix = f"**{ mod_tag } " if mod_tag else "**"
1475+ new_content = f"{ prefix } { author_name } :** { message } "
1476+ tasks += [m2 .edit (content = new_content )]
1477+ else :
1478+ embed2 = m2 .embeds [0 ]
1479+ embed2 .description = message
1480+ tasks += [m2 .edit (embed = embed2 )]
14861481
14871482 await asyncio .gather (* tasks )
14881483
0 commit comments