1
1
import logging
2
2
import typing as t
3
- from datetime import datetime , timedelta
3
+ from datetime import timedelta
4
+ from enum import Enum
4
5
6
+ import arrow
5
7
import discord
8
+ from arrow import Arrow
6
9
7
10
import bot
8
11
from bot import constants
15
18
EXCLUDED_CHANNELS = (constants .Channels .cooldown ,)
16
19
17
20
21
+ class ClosingReason (Enum ):
22
+ """All possible closing reasons for help channels."""
23
+
24
+ COMMAND = "command"
25
+ LATEST_MESSSAGE = "auto.latest_message"
26
+ CLAIMANT_TIMEOUT = "auto.claimant_timeout"
27
+ OTHER_TIMEOUT = "auto.other_timeout"
28
+ DELETED = "auto.deleted"
29
+ CLEANUP = "auto.cleanup"
30
+
31
+
18
32
def get_category_channels (category : discord .CategoryChannel ) -> t .Iterable [discord .TextChannel ]:
19
33
"""Yield the text channels of the `category` in an unsorted manner."""
20
34
log .trace (f"Getting text channels in the category '{ category } ' ({ category .id } )." )
@@ -25,23 +39,68 @@ def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[disco
25
39
yield channel
26
40
27
41
28
- async def get_idle_time (channel : discord .TextChannel ) -> t .Optional [ int ]:
42
+ async def get_closing_time (channel : discord .TextChannel , init_done : bool ) -> t .Tuple [ Arrow , ClosingReason ]:
29
43
"""
30
- Return the time elapsed, in seconds, since the last message sent in the `channel` .
44
+ Return the time at which the given help `channel` should be closed along with the reason .
31
45
32
- Return None if the channel has no messages.
46
+ `init_done` is True if the cog has finished loading and False otherwise.
47
+
48
+ The time is calculated as follows:
49
+
50
+ * If `init_done` is True or the cached time for the claimant's last message is unavailable,
51
+ add the configured `idle_minutes_claimant` to the time the most recent message was sent.
52
+ * If the help session is empty (see `is_empty`), do the above but with `deleted_idle_minutes`.
53
+ * If either of the above is attempted but the channel is completely empty, close the channel
54
+ immediately.
55
+ * Otherwise, retrieve the times of the claimant's and non-claimant's last messages from the
56
+ cache. Add the configured `idle_minutes_claimant` and idle_minutes_others`, respectively, and
57
+ choose the time which is furthest in the future.
33
58
"""
34
- log .trace (f"Getting the idle time for #{ channel } ({ channel .id } )." )
59
+ log .trace (f"Getting the closing time for #{ channel } ({ channel .id } )." )
60
+
61
+ is_empty = await _message .is_empty (channel )
62
+ if is_empty :
63
+ idle_minutes_claimant = constants .HelpChannels .deleted_idle_minutes
64
+ else :
65
+ idle_minutes_claimant = constants .HelpChannels .idle_minutes_claimant
66
+
67
+ claimant_time = await _caches .claimant_last_message_times .get (channel .id )
68
+
69
+ # The current session lacks messages, the cog is still starting, or the cache is empty.
70
+ if is_empty or not init_done or claimant_time is None :
71
+ msg = await _message .get_last_message (channel )
72
+ if not msg :
73
+ log .debug (f"No idle time available; #{ channel } ({ channel .id } ) has no messages, closing now." )
74
+ return Arrow .min , ClosingReason .DELETED
75
+
76
+ # Use the greatest offset to avoid the possibility of prematurely closing the channel.
77
+ time = Arrow .fromdatetime (msg .created_at ) + timedelta (minutes = idle_minutes_claimant )
78
+ return time , ClosingReason .LATEST_MESSSAGE
79
+
80
+ claimant_time = Arrow .utcfromtimestamp (claimant_time )
81
+ others_time = await _caches .non_claimant_last_message_times .get (channel .id )
82
+
83
+ if others_time :
84
+ others_time = Arrow .utcfromtimestamp (others_time )
85
+ else :
86
+ # The help session hasn't received any answers (messages from non-claimants) yet.
87
+ # Set to min value so it isn't considered when calculating the closing time.
88
+ others_time = Arrow .min
35
89
36
- msg = await _message .get_last_message (channel )
37
- if not msg :
38
- log .debug (f"No idle time available; #{ channel } ({ channel .id } ) has no messages." )
39
- return None
90
+ # Offset the cached times by the configured values.
91
+ others_time += timedelta (minutes = constants .HelpChannels .idle_minutes_others )
92
+ claimant_time += timedelta (minutes = idle_minutes_claimant )
40
93
41
- idle_time = (datetime .utcnow () - msg .created_at ).seconds
94
+ # Use the time which is the furthest into the future.
95
+ if claimant_time >= others_time :
96
+ closing_time = claimant_time
97
+ reason = ClosingReason .CLAIMANT_TIMEOUT
98
+ else :
99
+ closing_time = others_time
100
+ reason = ClosingReason .OTHER_TIMEOUT
42
101
43
- log .trace (f"#{ channel } ({ channel .id } ) has been idle for { idle_time } seconds ." )
44
- return idle_time
102
+ log .trace (f"#{ channel } ({ channel .id } ) should be closed at { closing_time } due to { reason } ." )
103
+ return closing_time , reason
45
104
46
105
47
106
async def get_in_use_time (channel_id : int ) -> t .Optional [timedelta ]:
@@ -50,8 +109,8 @@ async def get_in_use_time(channel_id: int) -> t.Optional[timedelta]:
50
109
51
110
claimed_timestamp = await _caches .claim_times .get (channel_id )
52
111
if claimed_timestamp :
53
- claimed = datetime .utcfromtimestamp (claimed_timestamp )
54
- return datetime .utcnow () - claimed
112
+ claimed = Arrow .utcfromtimestamp (claimed_timestamp )
113
+ return arrow .utcnow () - claimed
55
114
56
115
57
116
def is_excluded_channel (channel : discord .abc .GuildChannel ) -> bool :
0 commit comments