1919
2020
2121class ModMail (commands .Cog ):
22- # Session format per user_id:
23- # { 'thread_id': int, 'last_activity': ISO8601 timestamp }
22+ # Session format per user_id (best-effort; older persisted schemas may exist) :
23+ # { 'thread_id': int, 'last_activity': ISO8601 timestamp, 'state': 'open'|'closed'|'resolved' }
2424 modmail_sessions : Dict [int , Dict [str , Any ]] = {}
2525 _session_locks : Dict [int , asyncio .Lock ] = {}
2626 SESSIONS_FILE = Path ("data/modmail_sessions.json" )
@@ -111,6 +111,44 @@ async def _persist_sessions_to_file(self):
111111 except Exception :
112112 logger .exception ("modmail: failed to persist sessions to file" )
113113
114+ def _is_session_expired (self , session : Dict [str , Any ]) -> bool :
115+ reset_seconds = int (getattr (self .config , 'modmail_reset_seconds' , 0 ) or 0 )
116+ if reset_seconds <= 0 :
117+ return False
118+
119+ last_activity = session .get ('last_activity' )
120+ if not last_activity :
121+ return False
122+
123+ try :
124+ last_dt = datetime .fromisoformat (str (last_activity ))
125+ except Exception :
126+ return False
127+
128+ return (datetime .utcnow () - last_dt ) > timedelta (seconds = reset_seconds )
129+
130+ def _is_session_closed (self , session : Dict [str , Any ]) -> bool :
131+ state = str (session .get ('state' ) or '' ).lower ()
132+ return state in {'closed' , 'resolved' }
133+
134+ def _get_thread_from_session (
135+ self ,
136+ session : Dict [str , Any ],
137+ main_channel : discord .TextChannel ,
138+ ) -> Optional [discord .Thread ]:
139+ thread_id = session .get ('thread_id' )
140+ if not thread_id :
141+ return None
142+ try :
143+ thread = main_channel .get_thread (int (thread_id ))
144+ except Exception :
145+ return None
146+ if not thread :
147+ return None
148+ if getattr (thread , 'archived' , False ) or getattr (thread , 'locked' , False ):
149+ return None
150+ return thread
151+
114152 @commands .Cog .listener ()
115153 async def on_message (self , message : discord .Message ):
116154 if message .author .bot :
@@ -153,8 +191,15 @@ async def handle_dm_message(self, message: discord.Message):
153191
154192 webhook = await self ._get_or_create_webhook (main_channel )
155193
156- if not session :
157- # Create new session
194+ thread : Optional [discord .Thread ] = None
195+ session_active = False
196+ if session and isinstance (session , dict ):
197+ if not self ._is_session_closed (session ) and not self ._is_session_expired (session ):
198+ thread = self ._get_thread_from_session (session , main_channel )
199+ session_active = thread is not None
200+
201+ if not session_active :
202+ # Create new session (first-time or after closure/expiry)
158203 try :
159204 # Log to main channel first
160205 log_embed = discord .Embed (
@@ -173,12 +218,20 @@ async def handle_dm_message(self, message: discord.Message):
173218 await message .channel .send ("An error occurred while starting the modmail session." )
174219 return
175220
221+ assert thread is not None
222+
176223 # Notify user
177- await self ._send_dm_safe (message .author , embed = discord .Embed (
178- title = "ModMail Started" ,
179- description = "A session has been started with the moderators. Messages you send here will be forwarded to them." ,
180- color = discord .Color .default ()
181- ))
224+ await self ._send_dm_safe (
225+ message .author ,
226+ embed = discord .Embed (
227+ title = "ModMail Started" ,
228+ description = (
229+ "✅ Your message has been received and a new modmail session has been opened.\n "
230+ "Messages you send here will be forwarded to the moderators."
231+ ),
232+ color = discord .Color .default (),
233+ ),
234+ )
182235
183236 # Send initial message via webhook
184237 files = [await f .to_file () for f in message .attachments ]
@@ -191,29 +244,20 @@ async def handle_dm_message(self, message: discord.Message):
191244 files = files
192245 )
193246 except Exception as e :
194- await thread .send (f"Failed to relay message from user: { e } " )
247+ if thread is not None :
248+ await thread .send (f"Failed to relay message from user: { e } " )
195249 raise e
196250
197251 self .modmail_sessions [user_id ] = {
198252 'thread_id' : thread .id ,
199- 'last_activity' : datetime .utcnow ().isoformat ()
253+ 'last_activity' : datetime .utcnow ().isoformat (),
254+ 'state' : 'open'
200255 }
201256 else :
202257 # Continue session
203- thread_id = session .get ('thread_id' )
204- thread = None
205- if thread_id :
206- thread = main_channel .get_thread (int (thread_id ))
207-
208- if not thread :
209- # Thread deleted manually? Re-create
210- try :
211- thread = await main_channel .create_thread (name = f"ModMail - { message .author .name } ({ user_id } )" , type = discord .ChannelType .private_thread )
212- except discord .HTTPException :
213- thread = await main_channel .create_thread (name = f"ModMail - { message .author .name } ({ user_id } )" )
214-
215- session ['thread_id' ] = thread .id
216- await thread .send (f"Wait, previous thread was lost. Resuming session for { message .author .mention } ." )
258+ # `thread` is guaranteed by session_active
259+ assert thread is not None
260+ assert isinstance (session , dict )
217261
218262 files = [await f .to_file () for f in message .attachments ]
219263 try :
@@ -225,9 +269,11 @@ async def handle_dm_message(self, message: discord.Message):
225269 files = files
226270 )
227271 except Exception as e :
228- await thread .send (f"Failed to relay message from user: { e } " )
272+ if thread is not None :
273+ await thread .send (f"Failed to relay message from user: { e } " )
229274 raise e
230275 session ['last_activity' ] = datetime .utcnow ().isoformat ()
276+ session .setdefault ('state' , 'open' )
231277
232278 await self ._persist_sessions_to_file ()
233279 except Exception as e :
@@ -359,6 +405,7 @@ async def set_modmail_channel_slash(self, interaction: discord.Interaction, chan
359405 else :
360406 await interaction .response .send_message ("Please specify a text channel or use this in a text channel." , ephemeral = True )
361407 return
408+ assert channel is not None
362409 self .modmail_channel_id = channel .id
363410 await interaction .response .send_message (f"Modmail channel set to { channel .mention } ." , ephemeral = True )
364411
0 commit comments