From 13975b4d2dc83aa0b4846d76b0ab958aeb82bbcb Mon Sep 17 00:00:00 2001 From: B0nes <35849006+b0nes1@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:05:33 +0100 Subject: Implemented optional duration parameter in slowmode command --- bot/exts/moderation/slowmode.py | 114 ++++++++++++++++++++++++++++++++++------ 1 file changed, 99 insertions(+), 15 deletions(-) diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index 6efb710bb..296ae6742 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -1,14 +1,18 @@ +from datetime import UTC, datetime, timedelta from typing import Literal +from async_rediscache import RedisCache from dateutil.relativedelta import relativedelta from discord import TextChannel, Thread from discord.ext.commands import Cog, Context, group, has_any_role +from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot from bot.constants import Channels, Emojis, MODERATION_ROLES from bot.converters import DurationDelta from bot.log import get_logger from bot.utils import time +from bot.utils.time import TimestampFormats, discord_timestamp log = get_logger(__name__) @@ -26,8 +30,15 @@ MessageHolder = TextChannel | Thread | None class Slowmode(Cog): """Commands for getting and setting slowmode delays of text channels.""" + # Stores the expiration timestamp in POSIX format for active slowmodes, keyed by channel ID. + slowmode_expiration_cache = RedisCache() + + # Stores the original slowmode interval by channel ID, allowing its restoration after temporary slowmode expires. + original_slowmode_cache = RedisCache() + def __init__(self, bot: Bot) -> None: self.bot = bot + self.scheduler = Scheduler(self.__class__.__name__) @group(name="slowmode", aliases=["sm"], invoke_without_command=True) async def slowmode_group(self, ctx: Context) -> None: @@ -42,8 +53,14 @@ class Slowmode(Cog): channel = ctx.channel humanized_delay = time.humanize_delta(seconds=channel.slowmode_delay) - - await ctx.send(f"The slowmode delay for {channel.mention} is {humanized_delay}.") + if await self.slowmode_expiration_cache.contains(channel.id): + expiration_time = await self.slowmode_expiration_cache.get(channel.id) + expiration_timestamp = discord_timestamp(expiration_time, TimestampFormats.RELATIVE) + await ctx.send( + f"The slowmode delay for {channel.mention} is {humanized_delay} and expires in {expiration_timestamp}." + ) + else: + await ctx.send(f"The slowmode delay for {channel.mention} is {humanized_delay}.") @slowmode_group.command(name="set", aliases=["s"]) async def set_slowmode( @@ -51,8 +68,14 @@ class Slowmode(Cog): ctx: Context, channel: MessageHolder, delay: DurationDelta | Literal["0s", "0seconds"], + duration: DurationDelta | None = None ) -> None: - """Set the slowmode delay for a text channel.""" + """ + Set the slowmode delay for a text channel. + + Supports temporary slowmodes with the `duration` argument that automatically + revert to the original delay after expiration. + """ # Use the channel this command was invoked in if one was not given if channel is None: channel = ctx.channel @@ -66,37 +89,98 @@ class Slowmode(Cog): humanized_delay = time.humanize_delta(delay) # Ensure the delay is within discord's limits - if slowmode_delay <= SLOWMODE_MAX_DELAY: - log.info(f"{ctx.author} set the slowmode delay for #{channel} to {humanized_delay}.") - - await channel.edit(slowmode_delay=slowmode_delay) - if channel.id in COMMONLY_SLOWMODED_CHANNELS: - log.info(f"Recording slowmode change in stats for {channel.name}.") - self.bot.stats.gauge(f"slowmode.{COMMONLY_SLOWMODED_CHANNELS[channel.id]}", slowmode_delay) + if not slowmode_delay <= SLOWMODE_MAX_DELAY: + log.info( + f"{ctx.author} tried to set the slowmode delay of #{channel} to {humanized_delay}, " + "which is not between 0 and 6 hours." + ) await ctx.send( - f"{Emojis.check_mark} The slowmode delay for {channel.mention} is now {humanized_delay}." + f"{Emojis.cross_mark} The slowmode delay must be between 0 and 6 hours." ) + return - else: + if duration is not None: + slowmode_duration = time.relativedelta_to_timedelta(duration).total_seconds() + humanized_duration = time.humanize_delta(duration) + + expiration_time = datetime.now(tz=UTC) + timedelta(seconds=slowmode_duration) + expiration_timestamp = discord_timestamp(expiration_time, TimestampFormats.RELATIVE) + + # Only update original_slowmode_cache if the last slowmode was not temporary. + if not await self.slowmode_expiration_cache.contains(channel.id): + await self.original_slowmode_cache.set(channel.id, channel.slowmode_delay) + await self.slowmode_expiration_cache.set(channel.id, expiration_time.timestamp()) + + self.scheduler.schedule_at(expiration_time, channel.id, self._revert_slowmode(channel.id)) log.info( - f"{ctx.author} tried to set the slowmode delay of #{channel} to {humanized_delay}, " - "which is not between 0 and 6 hours." + f"{ctx.author} set the slowmode delay for #{channel} to" + f"{humanized_delay} which expires in {humanized_duration}." ) + await channel.edit(slowmode_delay=slowmode_delay) + await ctx.send( + f"{Emojis.check_mark} The slowmode delay for {channel.mention}" + f" is now {humanized_delay} and expires in {expiration_timestamp}." + ) + else: + if await self.slowmode_expiration_cache.contains(channel.id): + await self.slowmode_expiration_cache.delete(channel.id) + await self.original_slowmode_cache.delete(channel.id) + self.scheduler.cancel(channel.id) + log.info(f"{ctx.author} set the slowmode delay for #{channel} to {humanized_delay}.") + await channel.edit(slowmode_delay=slowmode_delay) await ctx.send( - f"{Emojis.cross_mark} The slowmode delay must be between 0 and 6 hours." + f"{Emojis.check_mark} The slowmode delay for {channel.mention} is now {humanized_delay}." ) + if channel.id in COMMONLY_SLOWMODED_CHANNELS: + log.info(f"Recording slowmode change in stats for {channel.name}.") + self.bot.stats.gauge(f"slowmode.{COMMONLY_SLOWMODED_CHANNELS[channel.id]}", slowmode_delay) + + async def _reschedule(self) -> None: + log.trace("Rescheduling the expiration of temporary slowmodes from cache.") + for channel_id, expiration in await self.slowmode_expiration_cache.items(): + expiration_datetime = datetime.fromtimestamp(expiration, tz=UTC) + channel = self.bot.get_channel(channel_id) + log.info(f"Rescheduling slowmode expiration for #{channel} ({channel_id}).") + self.scheduler.schedule_at(expiration_datetime, channel_id, self._revert_slowmode(channel_id)) + + async def _revert_slowmode(self, channel_id: int) -> None: + original_slowmode = await self.original_slowmode_cache.get(channel_id) + slowmode_delay = time.humanize_delta(seconds=original_slowmode) + channel = self.bot.get_channel(channel_id) + log.info(f"Slowmode in #{channel} ({channel.id}) has expired and has reverted to {slowmode_delay}.") + await channel.edit(slowmode_delay=original_slowmode) + await channel.send( + f"{Emojis.check_mark} A previously applied slowmode has expired and has been reverted to {slowmode_delay}." + ) + await self.slowmode_expiration_cache.delete(channel.id) + await self.original_slowmode_cache.delete(channel.id) @slowmode_group.command(name="reset", aliases=["r"]) async def reset_slowmode(self, ctx: Context, channel: MessageHolder) -> None: """Reset the slowmode delay for a text channel to 0 seconds.""" await self.set_slowmode(ctx, channel, relativedelta(seconds=0)) + if channel is None: + channel = ctx.channel + if await self.slowmode_expiration_cache.contains(channel.id): + await self.slowmode_expiration_cache.delete(channel.id) + await self.original_slowmode_cache.delete(channel.id) + self.scheduler.cancel(channel.id) async def cog_check(self, ctx: Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" return await has_any_role(*MODERATION_ROLES).predicate(ctx) + async def cog_load(self) -> None: + """Wait for guild to become available and reschedule slowmodes which should expire.""" + await self.bot.wait_until_guild_available() + await self._reschedule() + + async def cog_unload(self) -> None: + """Cancel all scheduled tasks.""" + self.scheduler.cancel_all() + async def setup(bot: Bot) -> None: """Load the Slowmode cog.""" -- cgit v1.2.3 From db5ee777ebd13ecdee8285b13f8493856b8a1292 Mon Sep 17 00:00:00 2001 From: B0nes <35849006+b0nes1@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:44:30 +0100 Subject: WIP: Added tests for slowmode command with duration argument. --- tests/bot/exts/moderation/test_slowmode.py | 112 ++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/tests/bot/exts/moderation/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py index cf5101e16..c9973703b 100644 --- a/tests/bot/exts/moderation/test_slowmode.py +++ b/tests/bot/exts/moderation/test_slowmode.py @@ -1,14 +1,15 @@ -import unittest +import datetime from unittest import mock from dateutil.relativedelta import relativedelta from bot.constants import Emojis from bot.exts.moderation.slowmode import Slowmode +from tests.base import RedisTestCase from tests.helpers import MockBot, MockContext, MockTextChannel -class SlowmodeTests(unittest.IsolatedAsyncioTestCase): +class SlowmodeTests(RedisTestCase): def setUp(self) -> None: self.bot = MockBot() @@ -95,6 +96,113 @@ class SlowmodeTests(unittest.IsolatedAsyncioTestCase): self.ctx, text_channel, relativedelta(seconds=0) ) + @mock.patch("bot.exts.moderation.slowmode.datetime") + async def test_set_slowmode_with_duration(self, mock_datetime) -> None: + """Set slowmode with a duration""" + mock_datetime.now.return_value = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) + test_cases = ( + ("python-general", 6, 6000, f"{Emojis.check_mark} The slowmode delay for #python-general is now 6 seconds" + " and expires in ."), + ("mod-spam", 5, 600, f"{Emojis.check_mark} The slowmode delay for #mod-spam is now 5 seconds and expires" + " in ."), + ("changelog", 12, 7200, f"{Emojis.check_mark} The slowmode delay for #changelog is now 12 seconds and" + " expires in .") + ) + for channel_name, seconds, duration, result_msg in test_cases: + with self.subTest( + channel_mention=channel_name, + seconds=seconds, + duration=duration, + result_msg=result_msg + ): + text_channel = MockTextChannel(name=channel_name, slowmode_delay=0) + await self.cog.set_slowmode( + self.cog, + self.ctx, + text_channel, + relativedelta(seconds=seconds), + duration=relativedelta(seconds=duration) + ) + text_channel.edit.assert_awaited_once_with(slowmode_delay=float(seconds)) + self.ctx.send.assert_called_once_with(result_msg) + self.ctx.reset_mock() + + @mock.patch("bot.exts.moderation.slowmode.datetime", wraps=datetime.datetime) + async def test_callback_scheduled(self, mock_datetime, ): + """Schedule slowmode to be reverted""" + mock_now = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) + mock_datetime.now.return_value = mock_now + self.cog.scheduler=mock.MagicMock() + + text_channel = MockTextChannel(name="python-general", slowmode_delay=2, id=123) + await self.cog.set_slowmode( + self.cog, + self.ctx, + text_channel, + relativedelta(seconds=4), + relativedelta(seconds=10)) + + args = (mock_now+relativedelta(seconds=10), text_channel.id, mock.ANY) + self.cog.scheduler.schedule_at.assert_called_once_with(*args) + + async def test_revert_slowmode_callback(self) -> None: + """Check that the slowmode is reverted""" + text_channel = MockTextChannel(name="python-general", slowmode_delay=2, id=123) + self.bot.get_channel = mock.MagicMock(return_value=text_channel) + await self.cog.set_slowmode( + self.cog, self.ctx, text_channel, relativedelta(seconds=4), relativedelta(seconds=10) + ) + await self.cog._revert_slowmode(text_channel.id) + text_channel.edit.assert_awaited_with(slowmode_delay=2) + text_channel.send.assert_called_once_with( + f"{Emojis.check_mark} A previously applied slowmode has expired and has been reverted to 2 seconds." + ) + + async def test_reschedule_slowmodes(self) -> None: + """Does not reschedule if cache is empty""" + self.cog.scheduler.schedule_at = mock.MagicMock() + self.cog._reschedule = mock.AsyncMock() + await self.cog.cog_unload() + await self.cog.cog_load() + + self.cog._reschedule.assert_called() + self.cog.scheduler.schedule_at.assert_not_called() + + + @mock.patch("bot.exts.moderation.slowmode.datetime", wraps=datetime.datetime) + async def test_reschedules_slowmodes(self, mock_datetime) -> None: + """Slowmodes are loaded from cache at cog reload and scheduled to be reverted.""" + mock_datetime.now.return_value = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) + mock_now = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) + self.cog._reschedule = mock.AsyncMock(wraps=self.cog._reschedule) + + channels = [] + slowmodes = ( + (123, (mock_now - datetime.timedelta(10)).timestamp(), 2), # expiration in the past + (456, (mock_now + datetime.timedelta(10)).timestamp(), 4), # expiration in the future + ) + + for channel_id, expiration_datetime, delay in slowmodes: + channel = MockTextChannel(slowmode_delay=delay, id=channel_id) + channels.append(channel) + + await self.cog.slowmode_expiration_cache.set(channel_id, expiration_datetime) + await self.cog.original_slowmode_cache.set(channel_id, delay) + + await self.cog.cog_unload() + await self.cog.cog_load() + + # check that _reschedule function was called upon cog reload. + self.cog._reschedule.assert_called() + + # check that a task was created for every cached slowmode. + for channel in channels: + self.assertIn(channel.id, self.cog.scheduler) + + # check that one channel with slowmode expiration in the past was edited immediately. + channels[0].edit.assert_awaited_once_with(slowmode_delay=channels[0].slowmode_delay) + channels[1].edit.assert_not_called() + @mock.patch("bot.exts.moderation.slowmode.has_any_role") @mock.patch("bot.exts.moderation.slowmode.MODERATION_ROLES", new=(1, 2, 3)) async def test_cog_check(self, role_check): -- cgit v1.2.3 From dd444feeac190c29906531629eab53360526502f Mon Sep 17 00:00:00 2001 From: B0nes <35849006+b0nes1@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:52:23 +0100 Subject: Fix issue with unawaited coroutine in test --- tests/bot/exts/moderation/test_slowmode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py index c9973703b..62246510b 100644 --- a/tests/bot/exts/moderation/test_slowmode.py +++ b/tests/bot/exts/moderation/test_slowmode.py @@ -132,7 +132,7 @@ class SlowmodeTests(RedisTestCase): """Schedule slowmode to be reverted""" mock_now = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) mock_datetime.now.return_value = mock_now - self.cog.scheduler=mock.MagicMock() + self.cog.scheduler=mock.MagicMock(wraps=self.cog.scheduler) text_channel = MockTextChannel(name="python-general", slowmode_delay=2, id=123) await self.cog.set_slowmode( -- cgit v1.2.3 From 875c6d26141e0e6490cbc2f8e6ec085e8b7b0487 Mon Sep 17 00:00:00 2001 From: B0nes <35849006+b0nes1@users.noreply.github.com> Date: Sun, 15 Jun 2025 02:27:45 +0100 Subject: Fix bug in test causing channel edit to not be recognised --- tests/bot/exts/moderation/test_slowmode.py | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/bot/exts/moderation/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py index 62246510b..3d816e144 100644 --- a/tests/bot/exts/moderation/test_slowmode.py +++ b/tests/bot/exts/moderation/test_slowmode.py @@ -1,3 +1,4 @@ +import asyncio import datetime from unittest import mock @@ -168,40 +169,41 @@ class SlowmodeTests(RedisTestCase): self.cog._reschedule.assert_called() self.cog.scheduler.schedule_at.assert_not_called() + async def test_reschedule_upon_reload(self) -> None: + """ Check that method `_reschedule` is called upon cog reload""" + self.cog._reschedule = mock.AsyncMock(wraps=self.cog._reschedule) + await self.cog.cog_unload() + await self.cog.cog_load() + + self.cog._reschedule.assert_called() @mock.patch("bot.exts.moderation.slowmode.datetime", wraps=datetime.datetime) async def test_reschedules_slowmodes(self, mock_datetime) -> None: """Slowmodes are loaded from cache at cog reload and scheduled to be reverted.""" mock_datetime.now.return_value = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) mock_now = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) - self.cog._reschedule = mock.AsyncMock(wraps=self.cog._reschedule) - channels = [] + channels = {} slowmodes = ( (123, (mock_now - datetime.timedelta(10)).timestamp(), 2), # expiration in the past - (456, (mock_now + datetime.timedelta(10)).timestamp(), 4), # expiration in the future + (456, (mock_now + datetime.timedelta(20)).timestamp(), 4), # expiration in the future ) for channel_id, expiration_datetime, delay in slowmodes: channel = MockTextChannel(slowmode_delay=delay, id=channel_id) - channels.append(channel) - + channels[channel_id] = channel await self.cog.slowmode_expiration_cache.set(channel_id, expiration_datetime) await self.cog.original_slowmode_cache.set(channel_id, delay) + self.bot.get_channel = mock.MagicMock(side_effect=lambda channel_id: channels.get(channel_id)) await self.cog.cog_unload() await self.cog.cog_load() + for channel_id in channels: + self.assertIn(channel_id, self.cog.scheduler) - # check that _reschedule function was called upon cog reload. - self.cog._reschedule.assert_called() - - # check that a task was created for every cached slowmode. - for channel in channels: - self.assertIn(channel.id, self.cog.scheduler) - - # check that one channel with slowmode expiration in the past was edited immediately. - channels[0].edit.assert_awaited_once_with(slowmode_delay=channels[0].slowmode_delay) - channels[1].edit.assert_not_called() + await asyncio.sleep(1) # give scheduled task time to execute + channels[123].edit.assert_awaited_once_with(slowmode_delay=channels[123].slowmode_delay) + channels[456].edit.assert_not_called() @mock.patch("bot.exts.moderation.slowmode.has_any_role") @mock.patch("bot.exts.moderation.slowmode.MODERATION_ROLES", new=(1, 2, 3)) -- cgit v1.2.3 From a15262df0c63027591a13e0024d2000986ae7c85 Mon Sep 17 00:00:00 2001 From: B0nes <35849006+b0nes1@users.noreply.github.com> Date: Sun, 29 Jun 2025 16:12:44 +0100 Subject: Refactor slowmode command to use Duration converter; Combine redis caches into one object; update and tidy related tests. --- bot/exts/moderation/slowmode.py | 66 +++++++++++++++--------------- tests/bot/exts/moderation/test_slowmode.py | 45 ++++++++++---------- 2 files changed, 55 insertions(+), 56 deletions(-) diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index 296ae6742..9caf776b1 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -1,4 +1,4 @@ -from datetime import UTC, datetime, timedelta +from datetime import datetime from typing import Literal from async_rediscache import RedisCache @@ -9,7 +9,7 @@ from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot from bot.constants import Channels, Emojis, MODERATION_ROLES -from bot.converters import DurationDelta +from bot.converters import Duration, DurationDelta from bot.log import get_logger from bot.utils import time from bot.utils.time import TimestampFormats, discord_timestamp @@ -30,11 +30,10 @@ MessageHolder = TextChannel | Thread | None class Slowmode(Cog): """Commands for getting and setting slowmode delays of text channels.""" - # Stores the expiration timestamp in POSIX format for active slowmodes, keyed by channel ID. - slowmode_expiration_cache = RedisCache() - - # Stores the original slowmode interval by channel ID, allowing its restoration after temporary slowmode expires. - original_slowmode_cache = RedisCache() + # RedisCache[discord.channel.id : f"{delay}, {expiry}"] + # `delay` is the slowmode delay assigned to the text channel. + # `expiry` is a naïve ISO 8601 string which describes when the slowmode should be removed. + slowmode_cache = RedisCache() def __init__(self, bot: Bot) -> None: self.bot = bot @@ -53,8 +52,8 @@ class Slowmode(Cog): channel = ctx.channel humanized_delay = time.humanize_delta(seconds=channel.slowmode_delay) - if await self.slowmode_expiration_cache.contains(channel.id): - expiration_time = await self.slowmode_expiration_cache.get(channel.id) + if await self.slowmode_cache.contains(channel.id): + expiration_time = await self.slowmode_cache.get(channel.id).split(", ")[1] expiration_timestamp = discord_timestamp(expiration_time, TimestampFormats.RELATIVE) await ctx.send( f"The slowmode delay for {channel.mention} is {humanized_delay} and expires in {expiration_timestamp}." @@ -68,12 +67,12 @@ class Slowmode(Cog): ctx: Context, channel: MessageHolder, delay: DurationDelta | Literal["0s", "0seconds"], - duration: DurationDelta | None = None + expiry: Duration | None = None ) -> None: """ Set the slowmode delay for a text channel. - Supports temporary slowmodes with the `duration` argument that automatically + Supports temporary slowmodes with the `expiry` argument that automatically revert to the original delay after expiration. """ # Use the channel this command was invoked in if one was not given @@ -100,22 +99,22 @@ class Slowmode(Cog): ) return - if duration is not None: - slowmode_duration = time.relativedelta_to_timedelta(duration).total_seconds() - humanized_duration = time.humanize_delta(duration) - - expiration_time = datetime.now(tz=UTC) + timedelta(seconds=slowmode_duration) - expiration_timestamp = discord_timestamp(expiration_time, TimestampFormats.RELATIVE) + if expiry is not None: + humanized_expiry = time.humanize_delta(expiry) + expiration_timestamp = discord_timestamp(expiry, TimestampFormats.RELATIVE) - # Only update original_slowmode_cache if the last slowmode was not temporary. - if not await self.slowmode_expiration_cache.contains(channel.id): - await self.original_slowmode_cache.set(channel.id, channel.slowmode_delay) - await self.slowmode_expiration_cache.set(channel.id, expiration_time.timestamp()) + # Only cache the original slowmode delay if there is not already an ongoing temporary slowmode. + if not await self.slowmode_cache.contains(channel.id): + await self.slowmode_cache.set(channel.id, f"{channel.slowmode_delay}, {expiry}") + else: + cached_delay = await self.slowmode_cache.get(channel.id) + await self.slowmode_cache.set(channel.id, f"{cached_delay}, {expiry}") + self.scheduler.cancel(channel.id) - self.scheduler.schedule_at(expiration_time, channel.id, self._revert_slowmode(channel.id)) + self.scheduler.schedule_at(expiry, channel.id, self._revert_slowmode(channel.id)) log.info( f"{ctx.author} set the slowmode delay for #{channel} to" - f"{humanized_delay} which expires in {humanized_duration}." + f"{humanized_delay} which expires in {humanized_expiry}." ) await channel.edit(slowmode_delay=slowmode_delay) await ctx.send( @@ -123,9 +122,8 @@ class Slowmode(Cog): f" is now {humanized_delay} and expires in {expiration_timestamp}." ) else: - if await self.slowmode_expiration_cache.contains(channel.id): - await self.slowmode_expiration_cache.delete(channel.id) - await self.original_slowmode_cache.delete(channel.id) + if await self.slowmode_cache.contains(channel.id): + await self.slowmode_cache.delete(channel.id) self.scheduler.cancel(channel.id) log.info(f"{ctx.author} set the slowmode delay for #{channel} to {humanized_delay}.") @@ -139,14 +137,16 @@ class Slowmode(Cog): async def _reschedule(self) -> None: log.trace("Rescheduling the expiration of temporary slowmodes from cache.") - for channel_id, expiration in await self.slowmode_expiration_cache.items(): - expiration_datetime = datetime.fromtimestamp(expiration, tz=UTC) + for channel_id, cached_data in await self.slowmode_cache.items(): + expiration = cached_data.split(", ")[1] + expiration_datetime = datetime.fromisoformat(expiration) channel = self.bot.get_channel(channel_id) log.info(f"Rescheduling slowmode expiration for #{channel} ({channel_id}).") self.scheduler.schedule_at(expiration_datetime, channel_id, self._revert_slowmode(channel_id)) async def _revert_slowmode(self, channel_id: int) -> None: - original_slowmode = await self.original_slowmode_cache.get(channel_id) + cached_data = await self.slowmode_cache.get(channel_id) + original_slowmode = int(cached_data.split(", ")[0]) slowmode_delay = time.humanize_delta(seconds=original_slowmode) channel = self.bot.get_channel(channel_id) log.info(f"Slowmode in #{channel} ({channel.id}) has expired and has reverted to {slowmode_delay}.") @@ -154,8 +154,7 @@ class Slowmode(Cog): await channel.send( f"{Emojis.check_mark} A previously applied slowmode has expired and has been reverted to {slowmode_delay}." ) - await self.slowmode_expiration_cache.delete(channel.id) - await self.original_slowmode_cache.delete(channel.id) + await self.slowmode_cache.delete(channel.id) @slowmode_group.command(name="reset", aliases=["r"]) async def reset_slowmode(self, ctx: Context, channel: MessageHolder) -> None: @@ -163,9 +162,8 @@ class Slowmode(Cog): await self.set_slowmode(ctx, channel, relativedelta(seconds=0)) if channel is None: channel = ctx.channel - if await self.slowmode_expiration_cache.contains(channel.id): - await self.slowmode_expiration_cache.delete(channel.id) - await self.original_slowmode_cache.delete(channel.id) + if await self.slowmode_cache.contains(channel.id): + await self.slowmode_cache.delete(channel.id) self.scheduler.cancel(channel.id) async def cog_check(self, ctx: Context) -> bool: diff --git a/tests/bot/exts/moderation/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py index 3d816e144..d88ffd784 100644 --- a/tests/bot/exts/moderation/test_slowmode.py +++ b/tests/bot/exts/moderation/test_slowmode.py @@ -98,9 +98,11 @@ class SlowmodeTests(RedisTestCase): ) @mock.patch("bot.exts.moderation.slowmode.datetime") - async def test_set_slowmode_with_duration(self, mock_datetime) -> None: - """Set slowmode with a duration""" - mock_datetime.now.return_value = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) + async def test_set_slowmode_with_expiry(self, mock_datetime) -> None: + """Set slowmode with an expiry""" + fixed_datetime = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) + mock_datetime.now.return_value = fixed_datetime + test_cases = ( ("python-general", 6, 6000, f"{Emojis.check_mark} The slowmode delay for #python-general is now 6 seconds" " and expires in ."), @@ -109,11 +111,11 @@ class SlowmodeTests(RedisTestCase): ("changelog", 12, 7200, f"{Emojis.check_mark} The slowmode delay for #changelog is now 12 seconds and" " expires in .") ) - for channel_name, seconds, duration, result_msg in test_cases: + for channel_name, seconds, expiry, result_msg in test_cases: with self.subTest( channel_mention=channel_name, seconds=seconds, - duration=duration, + expiry=expiry, result_msg=result_msg ): text_channel = MockTextChannel(name=channel_name, slowmode_delay=0) @@ -122,28 +124,27 @@ class SlowmodeTests(RedisTestCase): self.ctx, text_channel, relativedelta(seconds=seconds), - duration=relativedelta(seconds=duration) + fixed_datetime + relativedelta(seconds=expiry) ) text_channel.edit.assert_awaited_once_with(slowmode_delay=float(seconds)) self.ctx.send.assert_called_once_with(result_msg) self.ctx.reset_mock() - @mock.patch("bot.exts.moderation.slowmode.datetime", wraps=datetime.datetime) - async def test_callback_scheduled(self, mock_datetime, ): + async def test_callback_scheduled(self): """Schedule slowmode to be reverted""" - mock_now = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) - mock_datetime.now.return_value = mock_now self.cog.scheduler=mock.MagicMock(wraps=self.cog.scheduler) text_channel = MockTextChannel(name="python-general", slowmode_delay=2, id=123) + expiry = datetime.datetime.now(tz=datetime.UTC) + relativedelta(seconds=10) await self.cog.set_slowmode( self.cog, self.ctx, text_channel, relativedelta(seconds=4), - relativedelta(seconds=10)) + expiry + ) - args = (mock_now+relativedelta(seconds=10), text_channel.id, mock.ANY) + args = (expiry, text_channel.id, mock.ANY) self.cog.scheduler.schedule_at.assert_called_once_with(*args) async def test_revert_slowmode_callback(self) -> None: @@ -151,7 +152,11 @@ class SlowmodeTests(RedisTestCase): text_channel = MockTextChannel(name="python-general", slowmode_delay=2, id=123) self.bot.get_channel = mock.MagicMock(return_value=text_channel) await self.cog.set_slowmode( - self.cog, self.ctx, text_channel, relativedelta(seconds=4), relativedelta(seconds=10) + self.cog, + self.ctx, + text_channel, + relativedelta(seconds=4), + datetime.datetime.now(tz=datetime.UTC) + relativedelta(seconds=10) ) await self.cog._revert_slowmode(text_channel.id) text_channel.edit.assert_awaited_with(slowmode_delay=2) @@ -177,23 +182,19 @@ class SlowmodeTests(RedisTestCase): self.cog._reschedule.assert_called() - @mock.patch("bot.exts.moderation.slowmode.datetime", wraps=datetime.datetime) - async def test_reschedules_slowmodes(self, mock_datetime) -> None: + async def test_reschedules_slowmodes(self) -> None: """Slowmodes are loaded from cache at cog reload and scheduled to be reverted.""" - mock_datetime.now.return_value = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) - mock_now = datetime.datetime(2025, 6, 2, 12, 0, 0, tzinfo=datetime.UTC) + now = datetime.datetime.now(tz=datetime.UTC) channels = {} slowmodes = ( - (123, (mock_now - datetime.timedelta(10)).timestamp(), 2), # expiration in the past - (456, (mock_now + datetime.timedelta(20)).timestamp(), 4), # expiration in the future + (123, (now - datetime.timedelta(minutes=10)), 2), # expiration in the past + (456, (now + datetime.timedelta(minutes=20)), 4), # expiration in the future ) - for channel_id, expiration_datetime, delay in slowmodes: channel = MockTextChannel(slowmode_delay=delay, id=channel_id) channels[channel_id] = channel - await self.cog.slowmode_expiration_cache.set(channel_id, expiration_datetime) - await self.cog.original_slowmode_cache.set(channel_id, delay) + await self.cog.slowmode_cache.set(channel_id, f"{delay}, {expiration_datetime}") self.bot.get_channel = mock.MagicMock(side_effect=lambda channel_id: channels.get(channel_id)) await self.cog.cog_unload() -- cgit v1.2.3 From a798c38763e4fd95b4192528ab6e648e4ec4cf3e Mon Sep 17 00:00:00 2001 From: B0nes <35849006+b0nes1@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:32:49 +0100 Subject: General refactoring, change message formatting, update tests --- bot/exts/moderation/slowmode.py | 52 ++++++++++++++++-------------- tests/bot/exts/moderation/test_slowmode.py | 28 +++++++++------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index 9caf776b1..c03fc1e27 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -5,6 +5,7 @@ from async_rediscache import RedisCache from dateutil.relativedelta import relativedelta from discord import TextChannel, Thread from discord.ext.commands import Cog, Context, group, has_any_role +from pydis_core.utils.channel import get_or_fetch_channel from pydis_core.utils.scheduling import Scheduler from bot.bot import Bot @@ -12,7 +13,7 @@ from bot.constants import Channels, Emojis, MODERATION_ROLES from bot.converters import Duration, DurationDelta from bot.log import get_logger from bot.utils import time -from bot.utils.time import TimestampFormats, discord_timestamp +from bot.utils.time import format_relative, humanize_delta log = get_logger(__name__) @@ -52,11 +53,14 @@ class Slowmode(Cog): channel = ctx.channel humanized_delay = time.humanize_delta(seconds=channel.slowmode_delay) - if await self.slowmode_cache.contains(channel.id): - expiration_time = await self.slowmode_cache.get(channel.id).split(", ")[1] - expiration_timestamp = discord_timestamp(expiration_time, TimestampFormats.RELATIVE) + cached_data = await self.slowmode_cache.get(channel.id, None) + if cached_data is not None: + original_delay, expiration_time = cached_data.partition(", ") + humanized_original_delay = time.humanize_delta(seconds=int(original_delay)) + expiration_timestamp = format_relative(expiration_time) await ctx.send( - f"The slowmode delay for {channel.mention} is {humanized_delay} and expires in {expiration_timestamp}." + f"The slowmode delay for {channel.mention} is {humanized_delay}" + f" and will revert to {humanized_original_delay} {expiration_timestamp}." ) else: await ctx.send(f"The slowmode delay for {channel.mention} is {humanized_delay}.") @@ -88,7 +92,7 @@ class Slowmode(Cog): humanized_delay = time.humanize_delta(delay) # Ensure the delay is within discord's limits - if not slowmode_delay <= SLOWMODE_MAX_DELAY: + if slowmode_delay > SLOWMODE_MAX_DELAY: log.info( f"{ctx.author} tried to set the slowmode delay of #{channel} to {humanized_delay}, " "which is not between 0 and 6 hours." @@ -100,27 +104,28 @@ class Slowmode(Cog): return if expiry is not None: - humanized_expiry = time.humanize_delta(expiry) - expiration_timestamp = discord_timestamp(expiry, TimestampFormats.RELATIVE) + expiration_timestamp = format_relative(expiry) # Only cache the original slowmode delay if there is not already an ongoing temporary slowmode. if not await self.slowmode_cache.contains(channel.id): - await self.slowmode_cache.set(channel.id, f"{channel.slowmode_delay}, {expiry}") + delay_to_cache = channel.slowmode_delay else: - cached_delay = await self.slowmode_cache.get(channel.id) - await self.slowmode_cache.set(channel.id, f"{cached_delay}, {expiry}") + cached_data = await self.slowmode_cache.get(channel.id) + delay_to_cache = cached_data.split(", ")[0] self.scheduler.cancel(channel.id) + await self.slowmode_cache.set(channel.id, f"{delay_to_cache}, {expiry}") + humanized_original_delay = humanize_delta(seconds=int(delay_to_cache)) self.scheduler.schedule_at(expiry, channel.id, self._revert_slowmode(channel.id)) log.info( - f"{ctx.author} set the slowmode delay for #{channel} to" - f"{humanized_delay} which expires in {humanized_expiry}." + f"{ctx.author} set the slowmode delay for #{channel} to {humanized_delay}" + f" which will revert to {humanized_original_delay} in {humanize_delta(expiry)}." ) await channel.edit(slowmode_delay=slowmode_delay) await ctx.send( - f"{Emojis.check_mark} The slowmode delay for {channel.mention}" - f" is now {humanized_delay} and expires in {expiration_timestamp}." - ) + f"{Emojis.check_mark} The slowmode delay for {channel.mention}" + f" is now {humanized_delay} and will revert to {humanized_original_delay} {expiration_timestamp}." + ) else: if await self.slowmode_cache.contains(channel.id): await self.slowmode_cache.delete(channel.id) @@ -148,11 +153,13 @@ class Slowmode(Cog): cached_data = await self.slowmode_cache.get(channel_id) original_slowmode = int(cached_data.split(", ")[0]) slowmode_delay = time.humanize_delta(seconds=original_slowmode) - channel = self.bot.get_channel(channel_id) - log.info(f"Slowmode in #{channel} ({channel.id}) has expired and has reverted to {slowmode_delay}.") + channel = await get_or_fetch_channel(self.bot, channel_id) + mod_channel = await get_or_fetch_channel(self.bot, Channels.mods) + log.info(f"Slowmode in #{channel.name} ({channel.id}) has expired and has reverted to {slowmode_delay}.") await channel.edit(slowmode_delay=original_slowmode) - await channel.send( - f"{Emojis.check_mark} A previously applied slowmode has expired and has been reverted to {slowmode_delay}." + await mod_channel.send( + f"{Emojis.check_mark} A previously applied slowmode in {channel.jump_url} ({channel.id})" + f" has expired and has been reverted to {slowmode_delay}." ) await self.slowmode_cache.delete(channel.id) @@ -160,11 +167,6 @@ class Slowmode(Cog): async def reset_slowmode(self, ctx: Context, channel: MessageHolder) -> None: """Reset the slowmode delay for a text channel to 0 seconds.""" await self.set_slowmode(ctx, channel, relativedelta(seconds=0)) - if channel is None: - channel = ctx.channel - if await self.slowmode_cache.contains(channel.id): - await self.slowmode_cache.delete(channel.id) - self.scheduler.cancel(channel.id) async def cog_check(self, ctx: Context) -> bool: """Only allow moderators to invoke the commands in this cog.""" diff --git a/tests/bot/exts/moderation/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py index d88ffd784..212934242 100644 --- a/tests/bot/exts/moderation/test_slowmode.py +++ b/tests/bot/exts/moderation/test_slowmode.py @@ -104,12 +104,12 @@ class SlowmodeTests(RedisTestCase): mock_datetime.now.return_value = fixed_datetime test_cases = ( - ("python-general", 6, 6000, f"{Emojis.check_mark} The slowmode delay for #python-general is now 6 seconds" - " and expires in ."), - ("mod-spam", 5, 600, f"{Emojis.check_mark} The slowmode delay for #mod-spam is now 5 seconds and expires" - " in ."), - ("changelog", 12, 7200, f"{Emojis.check_mark} The slowmode delay for #changelog is now 12 seconds and" - " expires in .") + ("python-general", 6, 6000, f"{Emojis.check_mark} The slowmode delay for #python-general is now 6 seconds " + "and will revert to 0 seconds ."), + ("mod-spam", 5, 600, f"{Emojis.check_mark} The slowmode delay for #mod-spam is now 5 seconds and will " + "revert to 0 seconds ."), + ("changelog", 12, 7200, f"{Emojis.check_mark} The slowmode delay for #changelog is now 12 seconds and will " + "revert to 0 seconds .") ) for channel_name, seconds, expiry, result_msg in test_cases: with self.subTest( @@ -147,10 +147,15 @@ class SlowmodeTests(RedisTestCase): args = (expiry, text_channel.id, mock.ANY) self.cog.scheduler.schedule_at.assert_called_once_with(*args) - async def test_revert_slowmode_callback(self) -> None: + @mock.patch("bot.exts.moderation.slowmode.get_or_fetch_channel") + async def test_revert_slowmode_callback(self, mock_get_or_fetch_channel) -> None: """Check that the slowmode is reverted""" - text_channel = MockTextChannel(name="python-general", slowmode_delay=2, id=123) - self.bot.get_channel = mock.MagicMock(return_value=text_channel) + text_channel = MockTextChannel(name="python-general", slowmode_delay=2, id=123, jump_url="#python-general") + mod_channel = MockTextChannel(name="mods", id=999, ) + # mock.MagicMock(return_value=text_channel) + + mock_get_or_fetch_channel.side_effect = [text_channel, mod_channel] + await self.cog.set_slowmode( self.cog, self.ctx, @@ -160,8 +165,9 @@ class SlowmodeTests(RedisTestCase): ) await self.cog._revert_slowmode(text_channel.id) text_channel.edit.assert_awaited_with(slowmode_delay=2) - text_channel.send.assert_called_once_with( - f"{Emojis.check_mark} A previously applied slowmode has expired and has been reverted to 2 seconds." + mod_channel.send.assert_called_once_with( + f"{Emojis.check_mark} A previously applied slowmode in {text_channel.jump_url} ({text_channel.id}) " + "has expired and has been reverted to 2 seconds." ) async def test_reschedule_slowmodes(self) -> None: -- cgit v1.2.3 From 3af14eb13a0dd0ab1f67a6ecc7973c93a9c3a25a Mon Sep 17 00:00:00 2001 From: B0nes <35849006+b0nes1@users.noreply.github.com> Date: Tue, 1 Jul 2025 01:51:52 +0100 Subject: Remove stray comment --- tests/bot/exts/moderation/test_slowmode.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/bot/exts/moderation/test_slowmode.py b/tests/bot/exts/moderation/test_slowmode.py index 212934242..d75fcd2f1 100644 --- a/tests/bot/exts/moderation/test_slowmode.py +++ b/tests/bot/exts/moderation/test_slowmode.py @@ -152,8 +152,6 @@ class SlowmodeTests(RedisTestCase): """Check that the slowmode is reverted""" text_channel = MockTextChannel(name="python-general", slowmode_delay=2, id=123, jump_url="#python-general") mod_channel = MockTextChannel(name="mods", id=999, ) - # mock.MagicMock(return_value=text_channel) - mock_get_or_fetch_channel.side_effect = [text_channel, mod_channel] await self.cog.set_slowmode( -- cgit v1.2.3 From 0591b2c286d3baeb11600058f9fc10e53c629203 Mon Sep 17 00:00:00 2001 From: B0nes <35849006+b0nes1@users.noreply.github.com> Date: Tue, 1 Jul 2025 01:54:54 +0100 Subject: Use consistent time module import --- bot/exts/moderation/slowmode.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index c03fc1e27..3cab65ade 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -13,7 +13,6 @@ from bot.constants import Channels, Emojis, MODERATION_ROLES from bot.converters import Duration, DurationDelta from bot.log import get_logger from bot.utils import time -from bot.utils.time import format_relative, humanize_delta log = get_logger(__name__) @@ -57,7 +56,7 @@ class Slowmode(Cog): if cached_data is not None: original_delay, expiration_time = cached_data.partition(", ") humanized_original_delay = time.humanize_delta(seconds=int(original_delay)) - expiration_timestamp = format_relative(expiration_time) + expiration_timestamp = time.format_relative(expiration_time) await ctx.send( f"The slowmode delay for {channel.mention} is {humanized_delay}" f" and will revert to {humanized_original_delay} {expiration_timestamp}." @@ -104,7 +103,7 @@ class Slowmode(Cog): return if expiry is not None: - expiration_timestamp = format_relative(expiry) + expiration_timestamp = time.format_relative(expiry) # Only cache the original slowmode delay if there is not already an ongoing temporary slowmode. if not await self.slowmode_cache.contains(channel.id): @@ -114,12 +113,12 @@ class Slowmode(Cog): delay_to_cache = cached_data.split(", ")[0] self.scheduler.cancel(channel.id) await self.slowmode_cache.set(channel.id, f"{delay_to_cache}, {expiry}") - humanized_original_delay = humanize_delta(seconds=int(delay_to_cache)) + humanized_original_delay = time.humanize_delta(seconds=int(delay_to_cache)) self.scheduler.schedule_at(expiry, channel.id, self._revert_slowmode(channel.id)) log.info( f"{ctx.author} set the slowmode delay for #{channel} to {humanized_delay}" - f" which will revert to {humanized_original_delay} in {humanize_delta(expiry)}." + f" which will revert to {humanized_original_delay} in {time.humanize_delta(expiry)}." ) await channel.edit(slowmode_delay=slowmode_delay) await ctx.send( -- cgit v1.2.3 From 039e9175a91fb2b83d8d206e99ae1f311f24a051 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 9 Aug 2025 23:40:41 +0300 Subject: Centralize cache reading and decoding in a helper function --- bot/exts/moderation/slowmode.py | 51 ++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index 3cab65ade..a83692fa4 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -52,11 +52,8 @@ class Slowmode(Cog): channel = ctx.channel humanized_delay = time.humanize_delta(seconds=channel.slowmode_delay) - cached_data = await self.slowmode_cache.get(channel.id, None) - if cached_data is not None: - original_delay, expiration_time = cached_data.partition(", ") - humanized_original_delay = time.humanize_delta(seconds=int(original_delay)) - expiration_timestamp = time.format_relative(expiration_time) + original_delay, humanized_original_delay, expiration_timestamp = await self._fetch_sm_cache(channel.id) + if original_delay is not None: await ctx.send( f"The slowmode delay for {channel.mention} is {humanized_delay}" f" and will revert to {humanized_original_delay} {expiration_timestamp}." @@ -87,7 +84,7 @@ class Slowmode(Cog): if isinstance(delay, str): delay = relativedelta(seconds=0) - slowmode_delay = time.relativedelta_to_timedelta(delay).total_seconds() + slowmode_delay = int(time.relativedelta_to_timedelta(delay).total_seconds()) humanized_delay = time.humanize_delta(delay) # Ensure the delay is within discord's limits @@ -105,15 +102,14 @@ class Slowmode(Cog): if expiry is not None: expiration_timestamp = time.format_relative(expiry) - # Only cache the original slowmode delay if there is not already an ongoing temporary slowmode. - if not await self.slowmode_cache.contains(channel.id): - delay_to_cache = channel.slowmode_delay + original_delay, humanized_original_delay, _ = await self._fetch_sm_cache(channel.id) + # Cache the channel's current delay if it has no expiry, otherwise use the cached original delay. + if original_delay is None: + original_delay = channel.slowmode_delay + humanized_original_delay = time.humanize_delta(seconds=original_delay) else: - cached_data = await self.slowmode_cache.get(channel.id) - delay_to_cache = cached_data.split(", ")[0] self.scheduler.cancel(channel.id) - await self.slowmode_cache.set(channel.id, f"{delay_to_cache}, {expiry}") - humanized_original_delay = time.humanize_delta(seconds=int(delay_to_cache)) + await self.slowmode_cache.set(channel.id, f"{original_delay}, {expiry}") self.scheduler.schedule_at(expiry, channel.id, self._revert_slowmode(channel.id)) log.info( @@ -148,17 +144,34 @@ class Slowmode(Cog): log.info(f"Rescheduling slowmode expiration for #{channel} ({channel_id}).") self.scheduler.schedule_at(expiration_datetime, channel_id, self._revert_slowmode(channel_id)) + async def _fetch_sm_cache(self, channel_id: int) -> tuple[int | None, str, str]: + """ + Fetch the channel's info from the cache and decode it. + + If no cache for the channel, the returned slowmode is None. + """ + cached_data = await self.slowmode_cache.get(channel_id, None) + if not cached_data: + return None, "", "" + + original_delay, expiration_time = cached_data.split(", ") + original_delay = int(original_delay) + humanized_original_delay = time.humanize_delta(seconds=original_delay) + expiration_timestamp = time.format_relative(expiration_time) + + return original_delay, humanized_original_delay, expiration_timestamp + async def _revert_slowmode(self, channel_id: int) -> None: - cached_data = await self.slowmode_cache.get(channel_id) - original_slowmode = int(cached_data.split(", ")[0]) - slowmode_delay = time.humanize_delta(seconds=original_slowmode) + original_delay, humanized_original_delay, _ = await self._fetch_sm_cache(channel_id) channel = await get_or_fetch_channel(self.bot, channel_id) mod_channel = await get_or_fetch_channel(self.bot, Channels.mods) - log.info(f"Slowmode in #{channel.name} ({channel.id}) has expired and has reverted to {slowmode_delay}.") - await channel.edit(slowmode_delay=original_slowmode) + log.info( + f"Slowmode in #{channel.name} ({channel.id}) has expired and has reverted to {humanized_original_delay}." + ) + await channel.edit(slowmode_delay=original_delay) await mod_channel.send( f"{Emojis.check_mark} A previously applied slowmode in {channel.jump_url} ({channel.id})" - f" has expired and has been reverted to {slowmode_delay}." + f" has expired and has been reverted to {humanized_original_delay}." ) await self.slowmode_cache.delete(channel.id) -- cgit v1.2.3