2
2
import logging
3
3
import random
4
4
import typing as t
5
- from datetime import datetime , timezone
5
+ from datetime import timedelta
6
6
from operator import attrgetter
7
7
8
+ import arrow
8
9
import discord
9
10
import discord .abc
10
11
from discord .ext import commands
@@ -43,7 +44,9 @@ class HelpChannels(commands.Cog):
43
44
In Use Category
44
45
45
46
* Contains all channels which are occupied by someone needing help
46
- * Channel moves to dormant category after `constants.HelpChannels.idle_minutes` of being idle
47
+ * Channel moves to dormant category after
48
+ - `constants.HelpChannels.idle_minutes_other` minutes since the last user message, or
49
+ - `constants.HelpChannels.idle_minutes_claimant` minutes since the last claimant message.
47
50
* Command can prematurely mark a channel as dormant
48
51
* Channel claimant is allowed to use the command
49
52
* Allowed roles for the command are configurable with `constants.HelpChannels.cmd_whitelist`
@@ -70,7 +73,7 @@ def __init__(self, bot: Bot):
70
73
self .channel_queue : asyncio .Queue [discord .TextChannel ] = None
71
74
self .name_queue : t .Deque [str ] = None
72
75
73
- self .last_notification : t .Optional [datetime ] = None
76
+ self .last_notification : t .Optional [arrow . Arrow ] = None
74
77
75
78
# Asyncio stuff
76
79
self .queue_tasks : t .List [asyncio .Task ] = []
@@ -112,11 +115,13 @@ async def claim_channel(self, message: discord.Message) -> None:
112
115
113
116
self .bot .stats .incr ("help.claimed" )
114
117
115
- # Must use a timezone-aware datetime to ensure a correct POSIX timestamp.
116
- timestamp = datetime .now (timezone .utc ).timestamp ()
117
- await _caches .claim_times .set (message .channel .id , timestamp )
118
+ # datetime.timestamp() would assume it's local, despite d.py giving a (naïve) UTC time.
119
+ timestamp = arrow .Arrow .fromdatetime (message .created_at ).timestamp ()
118
120
119
- await _caches .unanswered .set (message .channel .id , True )
121
+ await _caches .claim_times .set (message .channel .id , timestamp )
122
+ await _caches .claimant_last_message_times .set (message .channel .id , timestamp )
123
+ # Delete to indicate that the help session has yet to receive an answer.
124
+ await _caches .non_claimant_last_message_times .delete (message .channel .id )
120
125
121
126
# Not awaited because it may indefinitely hold the lock while waiting for a channel.
122
127
scheduling .create_task (self .move_to_available (), name = f"help_claim_{ message .id } " )
@@ -187,7 +192,7 @@ async def close_command(self, ctx: commands.Context) -> None:
187
192
# Don't use a discord.py check because the check needs to fail silently.
188
193
if await self .close_check (ctx ):
189
194
log .info (f"Close command invoked by { ctx .author } in #{ ctx .channel } ." )
190
- await self .unclaim_channel (ctx .channel , is_auto = False )
195
+ await self .unclaim_channel (ctx .channel , closed_on = _channel . ClosingReason . COMMAND )
191
196
192
197
async def get_available_candidate (self ) -> discord .TextChannel :
193
198
"""
@@ -233,7 +238,7 @@ async def init_available(self) -> None:
233
238
elif missing < 0 :
234
239
log .trace (f"Moving { abs (missing )} superfluous available channels over to the Dormant category." )
235
240
for channel in channels [:abs (missing )]:
236
- await self .unclaim_channel (channel )
241
+ await self .unclaim_channel (channel , closed_on = _channel . ClosingReason . CLEANUP )
237
242
238
243
async def init_categories (self ) -> None :
239
244
"""Get the help category objects. Remove the cog if retrieval fails."""
@@ -293,26 +298,23 @@ async def move_idle_channel(self, channel: discord.TextChannel, has_task: bool =
293
298
"""
294
299
log .trace (f"Handling in-use channel #{ channel } ({ channel .id } )." )
295
300
296
- if not await _message .is_empty (channel ):
297
- idle_seconds = constants .HelpChannels .idle_minutes * 60
298
- else :
299
- idle_seconds = constants .HelpChannels .deleted_idle_minutes * 60
300
-
301
- time_elapsed = await _channel .get_idle_time (channel )
301
+ closing_time , closed_on = await _channel .get_closing_time (channel , self .init_task .done ())
302
302
303
- if time_elapsed is None or time_elapsed >= idle_seconds :
303
+ # Closing time is in the past.
304
+ # Add 1 second due to POSIX timestamps being lower resolution than datetime objects.
305
+ if closing_time < (arrow .utcnow () + timedelta (seconds = 1 )):
304
306
log .info (
305
- f"#{ channel } ({ channel .id } ) is idle longer than { idle_seconds } seconds "
306
- f"and will be made dormant."
307
+ f"#{ channel } ({ channel .id } ) is idle past { closing_time } "
308
+ f"and will be made dormant. Reason: { closed_on . value } "
307
309
)
308
310
309
- await self .unclaim_channel (channel )
311
+ await self .unclaim_channel (channel , closed_on = closed_on )
310
312
else :
311
313
# Cancel the existing task, if any.
312
314
if has_task :
313
315
self .scheduler .cancel (channel .id )
314
316
315
- delay = idle_seconds - time_elapsed
317
+ delay = ( closing_time - arrow . utcnow ()). seconds
316
318
log .info (
317
319
f"#{ channel } ({ channel .id } ) is still active; "
318
320
f"scheduling it to be moved after { delay } seconds."
@@ -356,15 +358,15 @@ async def move_to_dormant(self, channel: discord.TextChannel) -> None:
356
358
_stats .report_counts ()
357
359
358
360
@lock .lock_arg (f"{ NAMESPACE } .unclaim" , "channel" )
359
- async def unclaim_channel (self , channel : discord .TextChannel , * , is_auto : bool = True ) -> None :
361
+ async def unclaim_channel (self , channel : discord .TextChannel , * , closed_on : _channel . ClosingReason ) -> None :
360
362
"""
361
363
Unclaim an in-use help `channel` to make it dormant.
362
364
363
365
Unpin the claimant's question message and move the channel to the Dormant category.
364
366
Remove the cooldown role from the channel claimant if they have no other channels claimed.
365
367
Cancel the scheduled cooldown role removal task.
366
368
367
- Set `is_auto` to True if the channel was automatically closed or False if manually closed .
369
+ `closed_on` is the reason that the channel was closed. See _channel.ClosingReason for possible values .
368
370
"""
369
371
claimant_id = await _caches .claimants .get (channel .id )
370
372
_unclaim_channel = self ._unclaim_channel
@@ -375,9 +377,14 @@ async def unclaim_channel(self, channel: discord.TextChannel, *, is_auto: bool =
375
377
decorator = lock .lock_arg (f"{ NAMESPACE } .unclaim" , "claimant_id" , wait = True )
376
378
_unclaim_channel = decorator (_unclaim_channel )
377
379
378
- return await _unclaim_channel (channel , claimant_id , is_auto )
380
+ return await _unclaim_channel (channel , claimant_id , closed_on )
379
381
380
- async def _unclaim_channel (self , channel : discord .TextChannel , claimant_id : int , is_auto : bool ) -> None :
382
+ async def _unclaim_channel (
383
+ self ,
384
+ channel : discord .TextChannel ,
385
+ claimant_id : int ,
386
+ closed_on : _channel .ClosingReason
387
+ ) -> None :
381
388
"""Actual implementation of `unclaim_channel`. See that for full documentation."""
382
389
await _caches .claimants .delete (channel .id )
383
390
@@ -393,12 +400,12 @@ async def _unclaim_channel(self, channel: discord.TextChannel, claimant_id: int,
393
400
await _cooldown .remove_cooldown_role (claimant )
394
401
395
402
await _message .unpin (channel )
396
- await _stats .report_complete_session (channel .id , is_auto )
403
+ await _stats .report_complete_session (channel .id , closed_on )
397
404
await self .move_to_dormant (channel )
398
405
399
406
# Cancel the task that makes the channel dormant only if called by the close command.
400
407
# In other cases, the task is either already done or not-existent.
401
- if not is_auto :
408
+ if closed_on == _channel . ClosingReason . COMMAND :
402
409
self .scheduler .cancel (channel .id )
403
410
404
411
async def move_to_in_use (self , channel : discord .TextChannel ) -> None :
@@ -410,7 +417,7 @@ async def move_to_in_use(self, channel: discord.TextChannel) -> None:
410
417
category_id = constants .Categories .help_in_use ,
411
418
)
412
419
413
- timeout = constants .HelpChannels .idle_minutes * 60
420
+ timeout = constants .HelpChannels .idle_minutes_claimant * 60
414
421
415
422
log .trace (f"Scheduling #{ channel } ({ channel .id } ) to become dormant in { timeout } sec." )
416
423
self .scheduler .schedule_later (timeout , channel .id , self .move_idle_channel (channel ))
@@ -428,7 +435,7 @@ async def on_message(self, message: discord.Message) -> None:
428
435
if not _channel .is_excluded_channel (message .channel ):
429
436
await self .claim_channel (message )
430
437
else :
431
- await _message .check_for_answer (message )
438
+ await _message .update_message_caches (message )
432
439
433
440
@commands .Cog .listener ()
434
441
async def on_message_delete (self , msg : discord .Message ) -> None :
0 commit comments