Skip to content

Commit db32927

Browse files
committed
Implemented optional duration parameter in slowmode command
1 parent 27bf2fd commit db32927

1 file changed

Lines changed: 99 additions & 15 deletions

File tree

bot/exts/moderation/slowmode.py

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
from datetime import UTC, datetime, timedelta
12
from typing import Literal
23

4+
from async_rediscache import RedisCache
35
from dateutil.relativedelta import relativedelta
46
from discord import TextChannel, Thread
57
from discord.ext.commands import Cog, Context, group, has_any_role
8+
from pydis_core.utils.scheduling import Scheduler
69

710
from bot.bot import Bot
811
from bot.constants import Channels, Emojis, MODERATION_ROLES
912
from bot.converters import DurationDelta
1013
from bot.log import get_logger
1114
from bot.utils import time
15+
from bot.utils.time import TimestampFormats, discord_timestamp
1216

1317
log = get_logger(__name__)
1418

@@ -26,8 +30,15 @@
2630
class 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

101185
async def setup(bot: Bot) -> None:
102186
"""Load the Slowmode cog."""

0 commit comments

Comments
 (0)