@@ -146,6 +146,7 @@ def cancelled(self) -> bool:
146146 def cancelled (self , flag : bool ):
147147 self ._cancelled = flag
148148 if flag :
149+ self ._ready_event .set ()
149150 for i in self .wait_tasks :
150151 i .cancel ()
151152
@@ -260,9 +261,7 @@ async def snooze(self, moderator=None, command_used=None, snooze_for=None):
260261 {"channel_id" : str (self .channel .id )},
261262 {"$set" : {"snoozed" : True , "snooze_data" : self .snooze_data }},
262263 )
263- import logging
264-
265- logging .info (f"[SNOOZE] DB update result: { result .modified_count } " )
264+ logger .info ("[SNOOZE] DB update result: %s" , result .modified_count )
266265
267266 # Dispatch thread_snoozed event for plugins
268267 self .bot .dispatch ("thread_snoozed" , self , moderator , snooze_for )
@@ -728,9 +727,7 @@ async def _ensure_genesis(force: bool = False):
728727 "$unset" : {"snoozed" : "" , "snooze_data" : "" },
729728 },
730729 )
731- import logging
732-
733- logging .info (f"[UNSNOOZE] DB update result: { result .modified_count } " )
730+ logger .info ("[UNSNOOZE] DB update result: %s" , result .modified_count )
734731 # Notify in the configured channel
735732 notify_channel = self .bot .config .get ("unsnooze_notify_channel" ) or "thread"
736733 notify_text = self .bot .config .get ("unsnooze_text" ) or "This thread has been unsnoozed and restored."
@@ -1059,7 +1056,10 @@ async def close(
10591056 """Close a thread now or after a set time in seconds"""
10601057
10611058 # restarts the after timer
1062- await self .cancel_closure (auto_close )
1059+ await self .cancel_closure (
1060+ auto_close ,
1061+ mark_auto_close_cancelled = not auto_close ,
1062+ )
10631063
10641064 if after > 0 :
10651065 # TODO: Add somewhere to clean up broken closures
@@ -1103,7 +1103,7 @@ async def _close(self, closer, silent=False, delete_channel=True, message=None,
11031103 logger .error ("Thread already closed: %s." , e )
11041104 return
11051105
1106- await self .cancel_closure (all = True )
1106+ await self .cancel_closure (all = True , mark_auto_close_cancelled = False )
11071107
11081108 # Cancel auto closing the thread if closed by any means.
11091109
@@ -1277,18 +1277,32 @@ async def _disable_dm_creation_menu(self) -> None:
12771277 except Exception as inner_e :
12781278 logger .debug ("Failed removing view from DM menu message: %s" , inner_e )
12791279
1280- async def cancel_closure (self , auto_close : bool = False , all : bool = False ) -> None :
1280+ async def cancel_closure (
1281+ self ,
1282+ auto_close : bool = False ,
1283+ all : bool = False ,
1284+ * ,
1285+ mark_auto_close_cancelled : bool = True ,
1286+ ) -> None :
12811287 if self .close_task is not None and (not auto_close or all ):
12821288 self .close_task .cancel ()
12831289 self .close_task = None
12841290 if self .auto_close_task is not None and (auto_close or all ):
12851291 self .auto_close_task .cancel ()
12861292 self .auto_close_task = None
1287- self .auto_close_cancelled = True # Mark auto-close as explicitly cancelled
1288-
1289- to_update = self .bot .config ["closures" ].pop (str (self .id ), None )
1290- if to_update is not None :
1291- await self .bot .config .update ()
1293+ if mark_auto_close_cancelled :
1294+ self .auto_close_cancelled = True # Mark auto-close as explicitly cancelled
1295+
1296+ closure_key = str (self .id )
1297+ existing = self .bot .config ["closures" ].get (closure_key )
1298+ if existing is not None :
1299+ existing_is_auto = bool (existing .get ("auto_close" , False ))
1300+ should_remove = (
1301+ all or (auto_close and existing_is_auto ) or ((not auto_close ) and (not existing_is_auto ))
1302+ )
1303+ if should_remove :
1304+ self .bot .config ["closures" ].pop (closure_key , None )
1305+ await self .bot .config .update ()
12921306
12931307 async def _restart_close_timer (self ):
12941308 """
@@ -1798,6 +1812,13 @@ async def send(
17981812 reply commands to avoid mutating the original message object.
17991813 """
18001814 # Handle notes with Discord-like system message format - return early
1815+ if message is None :
1816+ # Safeguard against None messages (e.g. from menu interactions without a source message)
1817+ if not note and not from_mod and not thread_creation :
1818+ # If we're just trying to log/relay a user message and there is none, existing behavior
1819+ # suggests we might skip or error. Logging a warning and returning is safer than crashing.
1820+ return
1821+
18011822 if note :
18021823 destination = destination or self .channel
18031824 content = message .content or "[No content]"
@@ -1830,11 +1851,14 @@ async def send(
18301851 return await destination .send (embed = embed )
18311852
18321853 if not note and from_mod :
1833- # Only restart auto-close if it wasn't explicitly cancelled
1854+ # Only restart auto-close if it wasn't explicitly cancelled.
1855+ # Auto-close is driven by the last moderator reply.
18341856 if not self .auto_close_cancelled :
18351857 self .bot .loop .create_task (self ._restart_close_timer ()) # Start or restart thread auto close
18361858 elif not note and not from_mod :
1837- await self .cancel_closure (all = True )
1859+ # If the user replied last, the thread should not auto-close.
1860+ # Cancel any pending auto-close without marking it as an explicit cancellation.
1861+ await self .cancel_closure (auto_close = True , mark_auto_close_cancelled = False )
18381862
18391863 if self .close_task is not None :
18401864 # cancel closing if a thread message is sent.
@@ -1852,7 +1876,8 @@ async def send(
18521876 await self .wait_until_ready ()
18531877
18541878 if not from_mod and not note :
1855- self .bot .loop .create_task (self .bot .api .append_log (message , channel_id = self .channel .id ))
1879+ if self .channel :
1880+ self .bot .loop .create_task (self .bot .api .append_log (message , channel_id = self .channel .id ))
18561881
18571882 destination = destination or self .channel
18581883
@@ -2580,6 +2605,10 @@ async def create(
25802605 # checks for existing thread in cache
25812606 thread = self .cache .get (recipient .id )
25822607 if thread :
2608+ # If there's a pending menu, return the existing thread to avoid creating duplicates
2609+ if getattr (thread , "_pending_menu" , False ):
2610+ logger .debug ("Thread for %s has pending menu, returning existing thread." , recipient )
2611+ return thread
25832612 try :
25842613 await thread .wait_until_ready ()
25852614 except asyncio .CancelledError :
@@ -2588,8 +2617,8 @@ async def create(
25882617 label = f"{ recipient } ({ recipient .id } )"
25892618 except Exception :
25902619 label = f"User ({ getattr (recipient , 'id' , 'unknown' )} )"
2591- logger . warning ( "Thread for %s cancelled, abort creating." , label )
2592- return thread
2620+ self . cache . pop ( recipient . id , None )
2621+ thread = None
25932622 else :
25942623 if thread .channel and self .bot .get_channel (thread .channel .id ):
25952624 logger .warning ("Found an existing thread for %s, abort creating." , recipient )
@@ -2937,35 +2966,36 @@ async def callback(self, interaction: discord.Interaction):
29372966 setattr (self .outer_thread , "_pending_menu" , False )
29382967 return
29392968 # Forward the user's initial DM to the thread channel
2940- try :
2941- await self .outer_thread .send (message )
2942- except Exception :
2943- logger .error (
2944- "Failed to relay initial message after menu selection" ,
2945- exc_info = True ,
2946- )
2947- else :
2948- # React to the user's DM with the 'sent' emoji
2969+ if message :
29492970 try :
2950- (
2951- sent_emoji ,
2952- _ ,
2953- ) = await self .outer_thread .bot .retrieve_emoji ()
2954- await self .outer_thread .bot .add_reaction (message , sent_emoji )
2955- except Exception as e :
2956- logger .debug (
2957- "Failed to add sent reaction to user's DM: %s" ,
2958- e ,
2971+ await self .outer_thread .send (message )
2972+ except Exception :
2973+ logger .error (
2974+ "Failed to relay initial message after menu selection" ,
2975+ exc_info = True ,
2976+ )
2977+ else :
2978+ # React to the user's DM with the 'sent' emoji
2979+ try :
2980+ (
2981+ sent_emoji ,
2982+ _ ,
2983+ ) = await self .outer_thread .bot .retrieve_emoji ()
2984+ await self .outer_thread .bot .add_reaction (message , sent_emoji )
2985+ except Exception as e :
2986+ logger .debug (
2987+ "Failed to add sent reaction to user's DM: %s" ,
2988+ e ,
2989+ )
2990+ # Dispatch thread_reply event for parity
2991+ self .outer_thread .bot .dispatch (
2992+ "thread_reply" ,
2993+ self .outer_thread ,
2994+ False ,
2995+ message ,
2996+ False ,
2997+ False ,
29592998 )
2960- # Dispatch thread_reply event for parity
2961- self .outer_thread .bot .dispatch (
2962- "thread_reply" ,
2963- self .outer_thread ,
2964- False ,
2965- message ,
2966- False ,
2967- False ,
2968- )
29692999 # Clear pending flag
29703000 setattr (self .outer_thread , "_pending_menu" , False )
29713001 except Exception :
0 commit comments