1
+ from datetime import datetime
1
2
from typing import Literal
2
3
4
+ from async_rediscache import RedisCache
3
5
from dateutil .relativedelta import relativedelta
4
6
from discord import TextChannel , Thread
5
7
from discord .ext .commands import Cog , Context , group , has_any_role
8
+ from pydis_core .utils .channel import get_or_fetch_channel
9
+ from pydis_core .utils .scheduling import Scheduler
6
10
7
11
from bot .bot import Bot
8
12
from bot .constants import Channels , Emojis , MODERATION_ROLES
9
- from bot .converters import DurationDelta
13
+ from bot .converters import Duration , DurationDelta
10
14
from bot .log import get_logger
11
15
from bot .utils import time
12
16
26
30
class Slowmode (Cog ):
27
31
"""Commands for getting and setting slowmode delays of text channels."""
28
32
33
+ # RedisCache[discord.channel.id : f"{delay}, {expiry}"]
34
+ # `delay` is the slowmode delay assigned to the text channel.
35
+ # `expiry` is a naïve ISO 8601 string which describes when the slowmode should be removed.
36
+ slowmode_cache = RedisCache ()
37
+
29
38
def __init__ (self , bot : Bot ) -> None :
30
39
self .bot = bot
40
+ self .scheduler = Scheduler (self .__class__ .__name__ )
31
41
32
42
@group (name = "slowmode" , aliases = ["sm" ], invoke_without_command = True )
33
43
async def slowmode_group (self , ctx : Context ) -> None :
@@ -42,17 +52,29 @@ async def get_slowmode(self, ctx: Context, channel: MessageHolder) -> None:
42
52
channel = ctx .channel
43
53
44
54
humanized_delay = time .humanize_delta (seconds = channel .slowmode_delay )
45
-
46
- await ctx .send (f"The slowmode delay for { channel .mention } is { humanized_delay } ." )
55
+ original_delay , humanized_original_delay , expiration_timestamp = await self ._fetch_sm_cache (channel .id )
56
+ if original_delay is not None :
57
+ await ctx .send (
58
+ f"The slowmode delay for { channel .mention } is { humanized_delay } "
59
+ f" and will revert to { humanized_original_delay } { expiration_timestamp } ."
60
+ )
61
+ else :
62
+ await ctx .send (f"The slowmode delay for { channel .mention } is { humanized_delay } ." )
47
63
48
64
@slowmode_group .command (name = "set" , aliases = ["s" ])
49
65
async def set_slowmode (
50
66
self ,
51
67
ctx : Context ,
52
68
channel : MessageHolder ,
53
69
delay : DurationDelta | Literal ["0s" , "0seconds" ],
70
+ expiry : Duration | None = None
54
71
) -> None :
55
- """Set the slowmode delay for a text channel."""
72
+ """
73
+ Set the slowmode delay for a text channel.
74
+
75
+ Supports temporary slowmodes with the `expiry` argument that automatically
76
+ revert to the original delay after expiration.
77
+ """
56
78
# Use the channel this command was invoked in if one was not given
57
79
if channel is None :
58
80
channel = ctx .channel
@@ -62,31 +84,96 @@ async def set_slowmode(
62
84
if isinstance (delay , str ):
63
85
delay = relativedelta (seconds = 0 )
64
86
65
- slowmode_delay = time .relativedelta_to_timedelta (delay ).total_seconds ()
87
+ slowmode_delay = int ( time .relativedelta_to_timedelta (delay ).total_seconds () )
66
88
humanized_delay = time .humanize_delta (delay )
67
89
68
90
# Ensure the delay is within discord's limits
69
- if slowmode_delay <= SLOWMODE_MAX_DELAY :
70
- log .info (f"{ ctx .author } set the slowmode delay for #{ channel } to { humanized_delay } ." )
71
-
72
- await channel .edit (slowmode_delay = slowmode_delay )
73
- if channel .id in COMMONLY_SLOWMODED_CHANNELS :
74
- log .info (f"Recording slowmode change in stats for { channel .name } ." )
75
- self .bot .stats .gauge (f"slowmode.{ COMMONLY_SLOWMODED_CHANNELS [channel .id ]} " , slowmode_delay )
91
+ if slowmode_delay > SLOWMODE_MAX_DELAY :
92
+ log .info (
93
+ f"{ ctx .author } tried to set the slowmode delay of #{ channel } to { humanized_delay } , "
94
+ "which is not between 0 and 6 hours."
95
+ )
76
96
77
97
await ctx .send (
78
- f"{ Emojis .check_mark } The slowmode delay for { channel . mention } is now { humanized_delay } ."
98
+ f"{ Emojis .cross_mark } The slowmode delay must be between 0 and 6 hours ."
79
99
)
100
+ return
80
101
81
- else :
102
+ if expiry is not None :
103
+ expiration_timestamp = time .format_relative (expiry )
104
+
105
+ original_delay , humanized_original_delay , _ = await self ._fetch_sm_cache (channel .id )
106
+ # Cache the channel's current delay if it has no expiry, otherwise use the cached original delay.
107
+ if original_delay is None :
108
+ original_delay = channel .slowmode_delay
109
+ humanized_original_delay = time .humanize_delta (seconds = original_delay )
110
+ else :
111
+ self .scheduler .cancel (channel .id )
112
+ await self .slowmode_cache .set (channel .id , f"{ original_delay } , { expiry } " )
113
+
114
+ self .scheduler .schedule_at (expiry , channel .id , self ._revert_slowmode (channel .id ))
82
115
log .info (
83
- f"{ ctx .author } tried to set the slowmode delay of #{ channel } to { humanized_delay } , "
84
- "which is not between 0 and 6 hours."
116
+ f"{ ctx .author } set the slowmode delay for #{ channel } to { humanized_delay } "
117
+ f" which will revert to { humanized_original_delay } in { time .humanize_delta (expiry )} ."
118
+ )
119
+ await channel .edit (slowmode_delay = slowmode_delay )
120
+ await ctx .send (
121
+ f"{ Emojis .check_mark } The slowmode delay for { channel .mention } "
122
+ f" is now { humanized_delay } and will revert to { humanized_original_delay } { expiration_timestamp } ."
85
123
)
124
+ else :
125
+ if await self .slowmode_cache .contains (channel .id ):
126
+ await self .slowmode_cache .delete (channel .id )
127
+ self .scheduler .cancel (channel .id )
86
128
129
+ log .info (f"{ ctx .author } set the slowmode delay for #{ channel } to { humanized_delay } ." )
130
+ await channel .edit (slowmode_delay = slowmode_delay )
87
131
await ctx .send (
88
- f"{ Emojis .cross_mark } The slowmode delay must be between 0 and 6 hours ."
132
+ f"{ Emojis .check_mark } The slowmode delay for { channel . mention } is now { humanized_delay } ."
89
133
)
134
+ if channel .id in COMMONLY_SLOWMODED_CHANNELS :
135
+ log .info (f"Recording slowmode change in stats for { channel .name } ." )
136
+ self .bot .stats .gauge (f"slowmode.{ COMMONLY_SLOWMODED_CHANNELS [channel .id ]} " , slowmode_delay )
137
+
138
+ async def _reschedule (self ) -> None :
139
+ log .trace ("Rescheduling the expiration of temporary slowmodes from cache." )
140
+ for channel_id , cached_data in await self .slowmode_cache .items ():
141
+ expiration = cached_data .split (", " )[1 ]
142
+ expiration_datetime = datetime .fromisoformat (expiration )
143
+ channel = self .bot .get_channel (channel_id )
144
+ log .info (f"Rescheduling slowmode expiration for #{ channel } ({ channel_id } )." )
145
+ self .scheduler .schedule_at (expiration_datetime , channel_id , self ._revert_slowmode (channel_id ))
146
+
147
+ async def _fetch_sm_cache (self , channel_id : int ) -> tuple [int | None , str , str ]:
148
+ """
149
+ Fetch the channel's info from the cache and decode it.
150
+
151
+ If no cache for the channel, the returned slowmode is None.
152
+ """
153
+ cached_data = await self .slowmode_cache .get (channel_id , None )
154
+ if not cached_data :
155
+ return None , "" , ""
156
+
157
+ original_delay , expiration_time = cached_data .split (", " )
158
+ original_delay = int (original_delay )
159
+ humanized_original_delay = time .humanize_delta (seconds = original_delay )
160
+ expiration_timestamp = time .format_relative (expiration_time )
161
+
162
+ return original_delay , humanized_original_delay , expiration_timestamp
163
+
164
+ async def _revert_slowmode (self , channel_id : int ) -> None :
165
+ original_delay , humanized_original_delay , _ = await self ._fetch_sm_cache (channel_id )
166
+ channel = await get_or_fetch_channel (self .bot , channel_id )
167
+ mod_channel = await get_or_fetch_channel (self .bot , Channels .mods )
168
+ log .info (
169
+ f"Slowmode in #{ channel .name } ({ channel .id } ) has expired and has reverted to { humanized_original_delay } ."
170
+ )
171
+ await channel .edit (slowmode_delay = original_delay )
172
+ await mod_channel .send (
173
+ f"{ Emojis .check_mark } A previously applied slowmode in { channel .jump_url } ({ channel .id } )"
174
+ f" has expired and has been reverted to { humanized_original_delay } ."
175
+ )
176
+ await self .slowmode_cache .delete (channel .id )
90
177
91
178
@slowmode_group .command (name = "reset" , aliases = ["r" ])
92
179
async def reset_slowmode (self , ctx : Context , channel : MessageHolder ) -> None :
@@ -97,6 +184,15 @@ async def cog_check(self, ctx: Context) -> bool:
97
184
"""Only allow moderators to invoke the commands in this cog."""
98
185
return await has_any_role (* MODERATION_ROLES ).predicate (ctx )
99
186
187
+ async def cog_load (self ) -> None :
188
+ """Wait for guild to become available and reschedule slowmodes which should expire."""
189
+ await self .bot .wait_until_guild_available ()
190
+ await self ._reschedule ()
191
+
192
+ async def cog_unload (self ) -> None :
193
+ """Cancel all scheduled tasks."""
194
+ self .scheduler .cancel_all ()
195
+
100
196
101
197
async def setup (bot : Bot ) -> None :
102
198
"""Load the Slowmode cog."""
0 commit comments