1+ from datetime import UTC , datetime , timedelta
12from typing import Literal
23
4+ from async_rediscache import RedisCache
35from dateutil .relativedelta import relativedelta
46from discord import TextChannel , Thread
57from discord .ext .commands import Cog , Context , group , has_any_role
8+ from pydis_core .utils .scheduling import Scheduler
69
710from bot .bot import Bot
811from bot .constants import Channels , Emojis , MODERATION_ROLES
912from bot .converters import DurationDelta
1013from bot .log import get_logger
1114from bot .utils import time
15+ from bot .utils .time import TimestampFormats , discord_timestamp
1216
1317log = get_logger (__name__ )
1418
2630class Slowmode (Cog ):
2731 """Commands for getting and setting slowmode delays of text channels."""
2832
33+ # Stores the expiration timestamp in POSIX format for active slowmodes, keyed by channel ID.
34+ slowmode_expiration_cache = RedisCache ()
35+
36+ # Stores the original slowmode interval by channel ID, allowing its restoration after temporary slowmode expires.
37+ original_slowmode_cache = RedisCache ()
38+
2939 def __init__ (self , bot : Bot ) -> None :
3040 self .bot = bot
41+ self .scheduler = Scheduler (self .__class__ .__name__ )
3142
3243 @group (name = "slowmode" , aliases = ["sm" ], invoke_without_command = True )
3344 async def slowmode_group (self , ctx : Context ) -> None :
@@ -42,17 +53,29 @@ async def get_slowmode(self, ctx: Context, channel: MessageHolder) -> None:
4253 channel = ctx .channel
4354
4455 humanized_delay = time .humanize_delta (seconds = channel .slowmode_delay )
45-
46- await ctx .send (f"The slowmode delay for { channel .mention } is { humanized_delay } ." )
56+ if await self .slowmode_expiration_cache .contains (channel .id ):
57+ expiration_time = await self .slowmode_expiration_cache .get (channel .id )
58+ expiration_timestamp = discord_timestamp (expiration_time , TimestampFormats .RELATIVE )
59+ await ctx .send (
60+ f"The slowmode delay for { channel .mention } is { humanized_delay } and expires in { expiration_timestamp } ."
61+ )
62+ else :
63+ await ctx .send (f"The slowmode delay for { channel .mention } is { humanized_delay } ." )
4764
4865 @slowmode_group .command (name = "set" , aliases = ["s" ])
4966 async def set_slowmode (
5067 self ,
5168 ctx : Context ,
5269 channel : MessageHolder ,
5370 delay : DurationDelta | Literal ["0s" , "0seconds" ],
71+ duration : DurationDelta | None = None
5472 ) -> None :
55- """Set the slowmode delay for a text channel."""
73+ """
74+ Set the slowmode delay for a text channel.
75+
76+ Supports temporary slowmodes with the `duration` argument that automatically
77+ revert to the original delay after expiration.
78+ """
5679 # Use the channel this command was invoked in if one was not given
5780 if channel is None :
5881 channel = ctx .channel
@@ -66,37 +89,98 @@ async def set_slowmode(
6689 humanized_delay = time .humanize_delta (delay )
6790
6891 # 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 )
92+ if not slowmode_delay <= SLOWMODE_MAX_DELAY :
93+ log .info (
94+ f"{ ctx .author } tried to set the slowmode delay of #{ channel } to { humanized_delay } , "
95+ "which is not between 0 and 6 hours."
96+ )
7697
7798 await ctx .send (
78- f"{ Emojis .check_mark } The slowmode delay for { channel . mention } is now { humanized_delay } ."
99+ f"{ Emojis .cross_mark } The slowmode delay must be between 0 and 6 hours ."
79100 )
101+ return
80102
81- else :
103+ if duration is not None :
104+ slowmode_duration = time .relativedelta_to_timedelta (duration ).total_seconds ()
105+ humanized_duration = time .humanize_delta (duration )
106+
107+ expiration_time = datetime .now (tz = UTC ) + timedelta (seconds = slowmode_duration )
108+ expiration_timestamp = discord_timestamp (expiration_time , TimestampFormats .RELATIVE )
109+
110+ # Only update original_slowmode_cache if the last slowmode was not temporary.
111+ if not await self .slowmode_expiration_cache .contains (channel .id ):
112+ await self .original_slowmode_cache .set (channel .id , channel .slowmode_delay )
113+ await self .slowmode_expiration_cache .set (channel .id , expiration_time .timestamp ())
114+
115+ self .scheduler .schedule_at (expiration_time , channel .id , self ._revert_slowmode (channel .id ))
82116 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 ."
117+ f"{ ctx .author } set the slowmode delay for #{ channel } to"
118+ f" { humanized_delay } which expires in { humanized_duration } ."
85119 )
120+ await channel .edit (slowmode_delay = slowmode_delay )
121+ await ctx .send (
122+ f"{ Emojis .check_mark } The slowmode delay for { channel .mention } "
123+ f" is now { humanized_delay } and expires in { expiration_timestamp } ."
124+ )
125+ else :
126+ if await self .slowmode_expiration_cache .contains (channel .id ):
127+ await self .slowmode_expiration_cache .delete (channel .id )
128+ await self .original_slowmode_cache .delete (channel .id )
129+ self .scheduler .cancel (channel .id )
86130
131+ log .info (f"{ ctx .author } set the slowmode delay for #{ channel } to { humanized_delay } ." )
132+ await channel .edit (slowmode_delay = slowmode_delay )
87133 await ctx .send (
88- f"{ Emojis .cross_mark } The slowmode delay must be between 0 and 6 hours ."
134+ f"{ Emojis .check_mark } The slowmode delay for { channel . mention } is now { humanized_delay } ."
89135 )
136+ if channel .id in COMMONLY_SLOWMODED_CHANNELS :
137+ log .info (f"Recording slowmode change in stats for { channel .name } ." )
138+ self .bot .stats .gauge (f"slowmode.{ COMMONLY_SLOWMODED_CHANNELS [channel .id ]} " , slowmode_delay )
139+
140+ async def _reschedule (self ) -> None :
141+ log .trace ("Rescheduling the expiration of temporary slowmodes from cache." )
142+ for channel_id , expiration in await self .slowmode_expiration_cache .items ():
143+ expiration_datetime = datetime .fromtimestamp (expiration , tz = UTC )
144+ channel = self .bot .get_channel (channel_id )
145+ log .info (f"Rescheduling slowmode expiration for #{ channel } ({ channel_id } )." )
146+ self .scheduler .schedule_at (expiration_datetime , channel_id , self ._revert_slowmode (channel_id ))
147+
148+ async def _revert_slowmode (self , channel_id : int ) -> None :
149+ original_slowmode = await self .original_slowmode_cache .get (channel_id )
150+ slowmode_delay = time .humanize_delta (seconds = original_slowmode )
151+ channel = self .bot .get_channel (channel_id )
152+ log .info (f"Slowmode in #{ channel } ({ channel .id } ) has expired and has reverted to { slowmode_delay } ." )
153+ await channel .edit (slowmode_delay = original_slowmode )
154+ await channel .send (
155+ f"{ Emojis .check_mark } A previously applied slowmode has expired and has been reverted to { slowmode_delay } ."
156+ )
157+ await self .slowmode_expiration_cache .delete (channel .id )
158+ await self .original_slowmode_cache .delete (channel .id )
90159
91160 @slowmode_group .command (name = "reset" , aliases = ["r" ])
92161 async def reset_slowmode (self , ctx : Context , channel : MessageHolder ) -> None :
93162 """Reset the slowmode delay for a text channel to 0 seconds."""
94163 await self .set_slowmode (ctx , channel , relativedelta (seconds = 0 ))
164+ if channel is None :
165+ channel = ctx .channel
166+ if await self .slowmode_expiration_cache .contains (channel .id ):
167+ await self .slowmode_expiration_cache .delete (channel .id )
168+ await self .original_slowmode_cache .delete (channel .id )
169+ self .scheduler .cancel (channel .id )
95170
96171 async def cog_check (self , ctx : Context ) -> bool :
97172 """Only allow moderators to invoke the commands in this cog."""
98173 return await has_any_role (* MODERATION_ROLES ).predicate (ctx )
99174
175+ async def cog_load (self ) -> None :
176+ """Wait for guild to become available and reschedule slowmodes which should expire."""
177+ await self .bot .wait_until_guild_available ()
178+ await self ._reschedule ()
179+
180+ async def cog_unload (self ) -> None :
181+ """Cancel all scheduled tasks."""
182+ self .scheduler .cancel_all ()
183+
100184
101185async def setup (bot : Bot ) -> None :
102186 """Load the Slowmode cog."""
0 commit comments