From aff3e35a57cc231b73c7ffca568b20e8f83dd22b Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 22:29:22 +0000 Subject: ✏️`LATEST_MESSSAGE` -> `LATEST_MESSAGE` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_channel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index e43c1e789..940868245 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -22,7 +22,7 @@ class ClosingReason(Enum): """All possible closing reasons for help channels.""" COMMAND = "command" - LATEST_MESSSAGE = "auto.latest_message" + LATEST_MESSAGE = "auto.latest_message" CLAIMANT_TIMEOUT = "auto.claimant_timeout" OTHER_TIMEOUT = "auto.other_timeout" DELETED = "auto.deleted" @@ -75,7 +75,7 @@ async def get_closing_time(channel: discord.TextChannel, init_done: bool) -> t.T # Use the greatest offset to avoid the possibility of prematurely closing the channel. time = Arrow.fromdatetime(msg.created_at) + timedelta(minutes=idle_minutes_claimant) - reason = ClosingReason.DELETED if is_empty else ClosingReason.LATEST_MESSSAGE + reason = ClosingReason.DELETED if is_empty else ClosingReason.LATEST_MESSAGE return time, reason claimant_time = Arrow.utcfromtimestamp(claimant_time) -- cgit v1.2.3 From 73fe24f447c4626bbf8a2b7a235b8971f0d82f25 Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:05:31 +0000 Subject: πŸ”§ Add `notify_running_low` config values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config-default.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/config-default.yml b/config-default.yml index 583733fda..fcb1583a8 100644 --- a/config-default.yml +++ b/config-default.yml @@ -513,19 +513,16 @@ help_channels: # Prefix for help channel names name_prefix: 'help-' - # Notify if more available channels are needed but there are no more dormant ones - notify: true + notify_channel: *HELPERS # Channel in which to send notifications messages + notify_minutes: 15 # Minimum interval between helper notifications, used by both none_remaining and running_low - # Channel in which to send notifications - notify_channel: *HELPERS - - # Minimum interval between helper notifications - notify_minutes: 15 - - # Mention these roles in notifications - notify_roles: + notify_none_remaining: true # Pinging notification for the Helper role when no dormant channels remain + notify_none_remaining_roles: # Mention these roles in the non_remaining notification - *HELPERS_ROLE + notify_running_low: true # Non-pinging notification which is triggered when the channel count is equal or less than the threshold + notify_running_low_threshold: 4 # The amount of channels at which a running_low notification will be sent + redirect_output: delete_delay: 15 -- cgit v1.2.3 From 3f1089f701677233ee1f485979ed755c5285f5be Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:09:59 +0000 Subject: πŸ”§ Add `notify_running_low` config values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/constants.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 1b713a7e3..ecb1ed81b 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -619,10 +619,12 @@ class HelpChannels(metaclass=YAMLGetter): max_available: int max_total_channels: int name_prefix: str - notify: bool notify_channel: int notify_minutes: int - notify_roles: List[int] + notify_none_remaining: bool + notify_none_remaining_roles: List[int] + notify_running_low: bool + notify_running_low_threshold: int class RedirectOutput(metaclass=YAMLGetter): -- cgit v1.2.3 From d5606a7d49f420b82d1826f0f5d96fb88f3942ef Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:11:13 +0000 Subject: πŸ”§ Add `notify_running_low` config values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 241dd606c..cdc015a02 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -137,7 +137,7 @@ async def notify(channel: discord.TextChannel, last_notification: t.Optional[Arr * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_roles` - roles mentioned in notifications """ - if not constants.HelpChannels.notify: + if not constants.HelpChannels.notify_none_remaining: return log.trace("Notifying about lack of channels.") @@ -156,8 +156,8 @@ async def notify(channel: discord.TextChannel, last_notification: t.Optional[Arr try: log.trace("Sending notification message.") - mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_roles) - allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_roles] + mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) + allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] message = await channel.send( f"{mentions} A new available help channel is needed but there " -- cgit v1.2.3 From e91ab75cc66976b5cee8041a43944034cbcf4335 Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:14:45 +0000 Subject: ♻️Rename `notify` -> `notify_none_remaining` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 2 +- bot/exts/help_channels/_message.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 541c791e5..46f09f29a 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -236,7 +236,7 @@ class HelpChannels(commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") notify_channel = self.bot.get_channel(constants.HelpChannels.notify_channel) - last_notification = await _message.notify(notify_channel, self.last_notification) + last_notification = await _message.notify_none_remaining(notify_channel, self.last_notification) if last_notification: self.last_notification = last_notification self.bot.stats.incr("help.out_of_channel_alerts") diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index cdc015a02..70ae8b062 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -124,7 +124,7 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: +async def notify_none_remaining(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: """ Send a message in `channel` notifying about a lack of available help channels. -- cgit v1.2.3 From 289629cfcadf987efb28d8969516a22db52f4a77 Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:18:52 +0000 Subject: πŸ“ Update `notify_none_remaining` docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 70ae8b062..e21e9a450 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -126,16 +126,15 @@ async def dm_on_open(message: discord.Message) -> None: async def notify_none_remaining(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: """ - Send a message in `channel` notifying about a lack of available help channels. + Send a pinging message in `channel` notifying about there being no dormant channels remaining. If a notification was sent, return the time at which the message was sent. Otherwise, return None. Configuration: - - * `HelpChannels.notify` - toggle notifications - * `HelpChannels.notify_minutes` - minimum interval between notifications - * `HelpChannels.notify_roles` - roles mentioned in notifications + * `HelpChannels.notify_minutes` - minimum interval between notifications + * `HelpChannels.notify_none_remaining` - toggle notifications + * `HelpChannels.notify_none_remaining_roles` - roles mentioned in notifications """ if not constants.HelpChannels.notify_none_remaining: return -- cgit v1.2.3 From 9361f32aa8c3b201dc62edf0153b71c053411712 Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:25:36 +0000 Subject: πŸ’‘ Update docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index e21e9a450..b6b172e77 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -133,7 +133,7 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications - * `HelpChannels.notify_none_remaining` - toggle notifications + * `HelpChannels.notify_none_remaining` - toggle none_remaining notifications * `HelpChannels.notify_none_remaining_roles` - roles mentioned in notifications """ if not constants.HelpChannels.notify_none_remaining: @@ -171,6 +171,21 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: log.exception("Failed to send notification about lack of dormant channels!") +async def notify_running_low(): + """ + Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. + + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. + + Configuration: + * `HelpChannels.notify_minutes` - minimum interval between notifications + * `HelpChannels.notify_running_low` - toggle running_low notifications + * `HelpChannels.notify_running_low_threshold` - minimum amount of channels to trigger running_low notifications + """ + ... + + async def pin(message: discord.Message) -> None: """Pin an initial question `message` and store it in a cache.""" if await pin_wrapper(message.id, message.channel, pin=True): -- cgit v1.2.3 From 554e520aa0fbd928c02e90b3d263d64e3a4f6daf Mon Sep 17 00:00:00 2001 From: GDWR Date: Tue, 1 Feb 2022 00:24:12 +0000 Subject: ✨ Notify running low on channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 19 ++++++++++++++----- bot/exts/help_channels/_message.py | 38 ++++++++++++++------------------------ config-default.yml | 2 +- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 46f09f29a..85799516c 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -78,7 +78,10 @@ class HelpChannels(commands.Cog): self.channel_queue: asyncio.Queue[discord.TextChannel] = None self.name_queue: t.Deque[str] = None - self.last_notification: t.Optional[arrow.Arrow] = None + # Notifications + self.notify_interval_seconds = (constants.HelpChannels.notify_minutes * 60) + self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') + self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.dynamic_message: t.Optional[int] = None self.available_help_channels: t.Set[discord.TextChannel] = set() @@ -229,16 +232,22 @@ class HelpChannels(commands.Cog): try: channel = self.channel_queue.get_nowait() + + within_interval = (arrow.utcnow() - self.last_running_low_notification).seconds >= self.notify_interval_seconds + if within_interval and self.channel_queue.qsize() <= constants.HelpChannels.notify_running_low_threshold: + await _message.notify_running_low(self.bot.get_channel(constants.HelpChannels.notify_channel), self.channel_queue.qsize()) + self.last_running_low_notification = arrow.utcnow() + except asyncio.QueueEmpty: log.info("No candidate channels in the queue; creating a new channel.") channel = await self.create_dormant() if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") - notify_channel = self.bot.get_channel(constants.HelpChannels.notify_channel) - last_notification = await _message.notify_none_remaining(notify_channel, self.last_notification) - if last_notification: - self.last_notification = last_notification + + if (arrow.utcnow() - self.last_none_remaining_notification).seconds >= self.notify_interval_seconds: + await _message.notify_none_remaining(self.bot.get_channel(constants.HelpChannels.notify_channel)) + self.last_none_remaining_notification = arrow.utcnow() self.bot.stats.incr("help.out_of_channel_alerts") channel = await self.wait_for_dormant_channel() diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index b6b172e77..d53a03b77 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -124,13 +124,10 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify_none_remaining(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: +async def notify_none_remaining(channel: discord.TextChannel) -> None: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. - If a notification was sent, return the time at which the message was sent. - Otherwise, return None. - Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_none_remaining` - toggle none_remaining notifications @@ -141,49 +138,42 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: log.trace("Notifying about lack of channels.") - if last_notification: - elapsed = (arrow.utcnow() - last_notification).seconds - minimum_interval = constants.HelpChannels.notify_minutes * 60 - should_send = elapsed >= minimum_interval - else: - should_send = True - - if not should_send: - log.trace("Notification not sent because it's too recent since the previous one.") - return - try: log.trace("Sending notification message.") mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] - message = await channel.send( + await channel.send( f"{mentions} A new available help channel is needed but there " - f"are no more dormant ones. Consider freeing up some in-use channels manually by " + "are no more dormant ones. Consider freeing up some in-use channels manually by " f"using the `{constants.Bot.prefix}dormant` command within the channels.", allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles) ) - - return Arrow.fromdatetime(message.created_at) except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") -async def notify_running_low(): +async def notify_running_low(channel: discord.TextChannel, number_of_channels_left: int) -> None: """ Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. - - If a notification was sent, return the time at which the message was sent. - Otherwise, return None. + Including the amount of channels left in dormant. Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_running_low` - toggle running_low notifications * `HelpChannels.notify_running_low_threshold` - minimum amount of channels to trigger running_low notifications """ - ... + if not constants.HelpChannels.notify_running_low: + return + + log.trace("Notifying about getting close to no dormant channels.") + + await channel.send( + f"There are only {number_of_channels_left} dormant channels left. " + "Consider participating in some help channels so that we don't run out." + ) async def pin(message: discord.Message) -> None: diff --git a/config-default.yml b/config-default.yml index fcb1583a8..6ad471cbd 100644 --- a/config-default.yml +++ b/config-default.yml @@ -514,7 +514,7 @@ help_channels: name_prefix: 'help-' notify_channel: *HELPERS # Channel in which to send notifications messages - notify_minutes: 15 # Minimum interval between helper notifications, used by both none_remaining and running_low + notify_minutes: 15 # Minimum interval between none_remaining or running_low notifications notify_none_remaining: true # Pinging notification for the Helper role when no dormant channels remain notify_none_remaining_roles: # Mention these roles in the non_remaining notification -- cgit v1.2.3 From 9fa586a85f3e9707e13fa19919d1c8519227f087 Mon Sep 17 00:00:00 2001 From: GDWR Date: Tue, 1 Feb 2022 00:30:05 +0000 Subject: 🚨 Linting fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 8 ++++++-- bot/exts/help_channels/_message.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 85799516c..5dfb09b64 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -233,9 +233,13 @@ class HelpChannels(commands.Cog): try: channel = self.channel_queue.get_nowait() - within_interval = (arrow.utcnow() - self.last_running_low_notification).seconds >= self.notify_interval_seconds + time_since_last_notify_seconds = (arrow.utcnow() - self.last_running_low_notification).seconds + within_interval = time_since_last_notify_seconds >= self.notify_interval_seconds if within_interval and self.channel_queue.qsize() <= constants.HelpChannels.notify_running_low_threshold: - await _message.notify_running_low(self.bot.get_channel(constants.HelpChannels.notify_channel), self.channel_queue.qsize()) + await _message.notify_running_low( + self.bot.get_channel(constants.HelpChannels.notify_channel), + self.channel_queue.qsize() + ) self.last_running_low_notification = arrow.utcnow() except asyncio.QueueEmpty: diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index d53a03b77..7d70c9f00 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -1,7 +1,6 @@ import textwrap import typing as t -import arrow import discord from arrow import Arrow @@ -158,7 +157,8 @@ async def notify_none_remaining(channel: discord.TextChannel) -> None: async def notify_running_low(channel: discord.TextChannel, number_of_channels_left: int) -> None: """ Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. - Including the amount of channels left in dormant. + + This will include the number of dormant channels left `number_of_channels_left` Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications -- cgit v1.2.3 From 3bfd75bda26b73dde5a838d4f7bb979170db9304 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 18:41:16 +0000 Subject: πŸ‘Œ Remove redundant parenthesis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 5dfb09b64..41d5bbe72 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -79,7 +79,7 @@ class HelpChannels(commands.Cog): self.name_queue: t.Deque[str] = None # Notifications - self.notify_interval_seconds = (constants.HelpChannels.notify_minutes * 60) + self.notify_interval_seconds = constants.HelpChannels.notify_minutes * 60 self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') -- cgit v1.2.3 From e05f3c9124046a304ad402b54a786b7b9f3cdfb1 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 18:45:29 +0000 Subject: πŸ’‘ Comment usage of arbitrarily old date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 41d5bbe72..8c93b084d 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -80,6 +80,7 @@ class HelpChannels(commands.Cog): # Notifications self.notify_interval_seconds = constants.HelpChannels.notify_minutes * 60 + # Using a very old date so that we don't have to use Optional typing. self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') -- cgit v1.2.3 From 1a53b6b7ea9203e348f4b1b1678c30b140e16544 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 19:51:52 +0000 Subject: ♻️Move notifications into `_message.py` with predicate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 33 ++++++++++++------------ bot/exts/help_channels/_message.py | 52 ++++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 8c93b084d..aefa0718e 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -79,7 +79,6 @@ class HelpChannels(commands.Cog): self.name_queue: t.Deque[str] = None # Notifications - self.notify_interval_seconds = constants.HelpChannels.notify_minutes * 60 # Using a very old date so that we don't have to use Optional typing. self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') @@ -233,29 +232,31 @@ class HelpChannels(commands.Cog): try: channel = self.channel_queue.get_nowait() - - time_since_last_notify_seconds = (arrow.utcnow() - self.last_running_low_notification).seconds - within_interval = time_since_last_notify_seconds >= self.notify_interval_seconds - if within_interval and self.channel_queue.qsize() <= constants.HelpChannels.notify_running_low_threshold: - await _message.notify_running_low( - self.bot.get_channel(constants.HelpChannels.notify_channel), - self.channel_queue.qsize() - ) - self.last_running_low_notification = arrow.utcnow() - except asyncio.QueueEmpty: log.info("No candidate channels in the queue; creating a new channel.") channel = await self.create_dormant() if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") + last_notification = await _message.notify_none_remaining( + self.bot.get_channel(constants.HelpChannels.notify_channel), + self.last_none_remaining_notification + ) + + if last_notification: + self.last_none_remaining_notification = last_notification + + channel = await self.wait_for_dormant_channel() # Blocks until a new channel is available - if (arrow.utcnow() - self.last_none_remaining_notification).seconds >= self.notify_interval_seconds: - await _message.notify_none_remaining(self.bot.get_channel(constants.HelpChannels.notify_channel)) - self.last_none_remaining_notification = arrow.utcnow() - self.bot.stats.incr("help.out_of_channel_alerts") + else: + last_notification = await _message.notify_running_low( + self.bot.get_channel(constants.HelpChannels.notify_channel), + self.channel_queue.qsize(), + self.last_running_low_notification + ) - channel = await self.wait_for_dormant_channel() + if last_notification: + self.last_running_low_notification = last_notification return channel diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 7d70c9f00..097e648e0 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -1,6 +1,7 @@ import textwrap import typing as t +import arrow import discord from arrow import Arrow @@ -123,7 +124,7 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify_none_remaining(channel: discord.TextChannel) -> None: +async def notify_none_remaining(channel: discord.TextChannel, last_notification: Arrow) -> t.Optional[Arrow]: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. @@ -133,16 +134,18 @@ async def notify_none_remaining(channel: discord.TextChannel) -> None: * `HelpChannels.notify_none_remaining_roles` - roles mentioned in notifications """ if not constants.HelpChannels.notify_none_remaining: - return + return None - log.trace("Notifying about lack of channels.") + if (arrow.utcnow() - last_notification).seconds < (constants.HelpChannels.notify_minutes * 60): + log.trace("Did not send none_remaining notification as it hasn't been enough time since the last one.") + return None - try: - log.trace("Sending notification message.") + log.trace("Notifying about lack of channels.") - mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) - allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] + mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) + allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] + try: await channel.send( f"{mentions} A new available help channel is needed but there " "are no more dormant ones. Consider freeing up some in-use channels manually by " @@ -152,9 +155,16 @@ async def notify_none_remaining(channel: discord.TextChannel) -> None: except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") + finally: + bot.instance.stats.incr("help.out_of_channel_alerts") + return arrow.utcnow() -async def notify_running_low(channel: discord.TextChannel, number_of_channels_left: int) -> None: +async def notify_running_low( + channel: discord.TextChannel, + number_of_channels_left: int, + last_notification: Arrow +) -> t.Optional[Arrow]: """ Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. @@ -166,14 +176,28 @@ async def notify_running_low(channel: discord.TextChannel, number_of_channels_le * `HelpChannels.notify_running_low_threshold` - minimum amount of channels to trigger running_low notifications """ if not constants.HelpChannels.notify_running_low: - return + return None - log.trace("Notifying about getting close to no dormant channels.") + if number_of_channels_left > constants.HelpChannels.notify_running_low_threshold: + log.trace("Did not send notify_running_low notification as the threshold was not met.") + return None - await channel.send( - f"There are only {number_of_channels_left} dormant channels left. " - "Consider participating in some help channels so that we don't run out." - ) + if (arrow.utcnow() - last_notification).seconds < (constants.HelpChannels.notify_minutes * 60): + log.trace("Did not send notify_running_low notification as it hasn't been enough time since the last one.") + return None + + log.trace("Notifying about getting close to no dormant channels.") + try: + await channel.send( + f"There are only {number_of_channels_left} dormant channels left. " + "Consider participating in some help channels so that we don't run out." + ) + except Exception: + # Handle it here cause this feature isn't critical for the functionality of the system. + log.exception("Failed to send notification about running low of dormant channels!") + finally: + bot.instance.stats.incr("help.running_low_alerts") + return arrow.utcnow() async def pin(message: discord.Message) -> None: -- cgit v1.2.3 From e02aee662043888ffcb982af694cea9da07403f2 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 19:57:12 +0000 Subject: πŸ‘Œ Remove the need to pass in channel via arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 11 ++--------- bot/exts/help_channels/_message.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index aefa0718e..78b01aa03 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -238,10 +238,7 @@ class HelpChannels(commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") - last_notification = await _message.notify_none_remaining( - self.bot.get_channel(constants.HelpChannels.notify_channel), - self.last_none_remaining_notification - ) + last_notification = await _message.notify_none_remaining(self.last_none_remaining_notification) if last_notification: self.last_none_remaining_notification = last_notification @@ -249,11 +246,7 @@ class HelpChannels(commands.Cog): channel = await self.wait_for_dormant_channel() # Blocks until a new channel is available else: - last_notification = await _message.notify_running_low( - self.bot.get_channel(constants.HelpChannels.notify_channel), - self.channel_queue.qsize(), - self.last_running_low_notification - ) + last_notification = await _message.notify_running_low(self.channel_queue.qsize(), self.last_running_low_notification) if last_notification: self.last_running_low_notification = last_notification diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 097e648e0..554aac7b4 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -124,7 +124,7 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify_none_remaining(channel: discord.TextChannel, last_notification: Arrow) -> t.Optional[Arrow]: +async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. @@ -145,6 +145,10 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] + channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) + if channel is None: + log.trace("Did not send none_remaining notification as the notification channel couldn't be gathered.") + try: await channel.send( f"{mentions} A new available help channel is needed but there " @@ -160,11 +164,7 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: return arrow.utcnow() -async def notify_running_low( - channel: discord.TextChannel, - number_of_channels_left: int, - last_notification: Arrow -) -> t.Optional[Arrow]: +async def notify_running_low(number_of_channels_left: int, last_notification: Arrow) -> t.Optional[Arrow]: """ Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. @@ -187,6 +187,11 @@ async def notify_running_low( return None log.trace("Notifying about getting close to no dormant channels.") + + channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) + if channel is None: + log.trace("Did not send notify_running notification as the notification channel couldn't be gathered.") + try: await channel.send( f"There are only {number_of_channels_left} dormant channels left. " -- cgit v1.2.3 From e566a206b67af48c42159a5c0969fbddee76da1d Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 20:11:05 +0000 Subject: 🚨 Linting fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 5 ++++- bot/exts/help_channels/_message.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 78b01aa03..0fd631a6e 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -246,7 +246,10 @@ class HelpChannels(commands.Cog): channel = await self.wait_for_dormant_channel() # Blocks until a new channel is available else: - last_notification = await _message.notify_running_low(self.channel_queue.qsize(), self.last_running_low_notification) + last_notification = await _message.notify_running_low( + self.channel_queue.qsize(), + self.last_running_low_notification + ) if last_notification: self.last_running_low_notification = last_notification diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 554aac7b4..d867dd93d 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -161,7 +161,8 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: log.exception("Failed to send notification about lack of dormant channels!") finally: bot.instance.stats.incr("help.out_of_channel_alerts") - return arrow.utcnow() + + return arrow.utcnow() async def notify_running_low(number_of_channels_left: int, last_notification: Arrow) -> t.Optional[Arrow]: @@ -202,7 +203,8 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar log.exception("Failed to send notification about running low of dormant channels!") finally: bot.instance.stats.incr("help.running_low_alerts") - return arrow.utcnow() + + return arrow.utcnow() async def pin(message: discord.Message) -> None: -- cgit v1.2.3 From dab2673d5e6ba387340efc5b9e4ddebb85795903 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 20:18:41 +0000 Subject: πŸ’‘ Update docstrings to reflect changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index d867dd93d..f8f10f774 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -128,6 +128,9 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. + Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_none_remaining` - toggle none_remaining notifications @@ -171,6 +174,9 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar This will include the number of dormant channels left `number_of_channels_left` + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. + Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_running_low` - toggle running_low notifications -- cgit v1.2.3 From d10b7d2c56d3530b69bf96c02e55ed0248a3f026 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Mon, 14 Feb 2022 16:17:03 +0000 Subject: Fix ignoring of raw DM edits (#2085) --- bot/exts/moderation/modlog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 2c01a4a21..54a08738c 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -729,6 +729,9 @@ class ModLog(Cog, name="ModLog"): @Cog.listener() async def on_raw_message_edit(self, event: discord.RawMessageUpdateEvent) -> None: """Log raw message edit event to message change log.""" + if event.guild_id is None: + return # ignore DM edits + await self.bot.wait_until_guild_available() try: channel = self.bot.get_channel(int(event.data["channel_id"])) -- cgit v1.2.3 From da58a4a3338f78c9263947a3d4433c9c56d37a02 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 16 Feb 2022 22:06:54 +0000 Subject: Fix: `!raw` can now be used in threads (#2090) --- bot/exts/info/information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 5b25fd0c3..e616b9208 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -470,7 +470,7 @@ class Information(Cog): If `json` is True, send the information in a copy-pasteable Python format. """ - if ctx.author not in message.channel.members: + if not message.channel.permissions_for(ctx.author).read_messages: await ctx.send(":x: You do not have permissions to see the channel this message is in.") return -- cgit v1.2.3 From 465c2f5f73e743d11892ebd8dd4421d35e599dd4 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:12:05 +0000 Subject: Reply with log url after cleaning messages If done outside a mod channel, it instead tags the invoker in #mods. --- bot/constants.py | 1 + bot/exts/moderation/clean.py | 36 ++++++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 1b713a7e3..77c01bfa3 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -445,6 +445,7 @@ class Channels(metaclass=YAMLGetter): incidents_archive: int mod_alerts: int mod_meta: int + mods: int nominations: int nomination_voting: int organisation: int diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index e61ef7880..f8ba230b3 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -331,12 +331,17 @@ class Clean(Cog): return deleted - async def _modlog_cleaned_messages(self, messages: list[Message], channels: CleanChannels, ctx: Context) -> bool: - """Log the deleted messages to the modlog. Return True if logging was successful.""" + async def _modlog_cleaned_messages( + self, + messages: list[Message], + channels: CleanChannels, + ctx: Context + ) -> Optional[str]: + """Log the deleted messages to the modlog, returning the log url if logging was successful.""" if not messages: # Can't build an embed, nothing to clean! await self._send_expiring_message(ctx, ":x: No matching messages could be found.") - return False + return None # Reverse the list to have reverse chronological order log_messages = reversed(messages) @@ -362,7 +367,7 @@ class Clean(Cog): channel_id=Channels.mod_log, ) - return True + return log_url # endregion @@ -375,8 +380,8 @@ class Clean(Cog): regex: Optional[re.Pattern] = None, first_limit: Optional[CleanLimit] = None, second_limit: Optional[CleanLimit] = None, - ) -> None: - """A helper function that does the actual message cleaning.""" + ) -> Optional[str]: + """A helper function that does the actual message cleaning, returns the log url if logging was successful.""" self._validate_input(channels, bots_only, users, first_limit, second_limit) # Are we already performing a clean? @@ -384,7 +389,7 @@ class Clean(Cog): await self._send_expiring_message( ctx, ":x: Please wait for the currently ongoing clean operation to complete." ) - return + return None self.cleaning = True deletion_channels = self._channels_set(channels, ctx, first_limit, second_limit) @@ -418,7 +423,7 @@ class Clean(Cog): if not self.cleaning: # Means that the cleaning was canceled - return + return None # Now let's delete the actual messages with purge. self.mod_log.ignore(Event.message_delete, *message_ids) @@ -427,11 +432,18 @@ class Clean(Cog): if not channels: channels = deletion_channels - logged = await self._modlog_cleaned_messages(deleted_messages, channels, ctx) + log_url = await self._modlog_cleaned_messages(deleted_messages, channels, ctx) - if logged and is_mod_channel(ctx.channel): - with suppress(NotFound): # Can happen if the invoker deleted their own messages. - await ctx.message.add_reaction(Emojis.check_mark) + success_message = ( + f"{Emojis.ok_hand} Deleted {len(deleted_messages)} messages. " + f"A log of the deleted messages can be found here {log_url}." + ) + if log_url and is_mod_channel(ctx.channel): + await ctx.reply(success_message) + elif log_url: + if mods := self.bot.get_channel(Channels.mods): + await mods.send(f"{ctx.author.mention} {success_message}") + return log_url # region: Commands -- cgit v1.2.3 From caaf0fa6d73ff6c8cfe54c7f8b952caf0aec97e2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:13:09 +0000 Subject: Support not deleting invoking message of a clean task --- bot/exts/moderation/clean.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index f8ba230b3..cb6836258 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -380,6 +380,7 @@ class Clean(Cog): regex: Optional[re.Pattern] = None, first_limit: Optional[CleanLimit] = None, second_limit: Optional[CleanLimit] = None, + attempt_delete_invocation: bool = True, ) -> Optional[str]: """A helper function that does the actual message cleaning, returns the log url if logging was successful.""" self._validate_input(channels, bots_only, users, first_limit, second_limit) @@ -404,8 +405,9 @@ class Clean(Cog): # Needs to be called after standardizing the input. predicate = self._build_predicate(first_limit, second_limit, bots_only, users, regex) - # Delete the invocation first - await self._delete_invocation(ctx) + if attempt_delete_invocation: + # Delete the invocation first + await self._delete_invocation(ctx) if self._use_cache(first_limit): log.trace(f"Messages for cleaning by {ctx.author.id} will be searched in the cache.") -- cgit v1.2.3 From 3a7871b839a1ff13fc95be562eb275676ed41b7f Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:14:44 +0000 Subject: Update respect_role_hierarchy decorator to pass through return values --- bot/decorators.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/decorators.py b/bot/decorators.py index 048a2a09a..f4331264f 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -188,7 +188,7 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: """ def decorator(func: types.FunctionType) -> types.FunctionType: @command_wraps(func) - async def wrapper(*args, **kwargs) -> None: + async def wrapper(*args, **kwargs) -> t.Any: log.trace(f"{func.__name__}: respect role hierarchy decorator called") bound_args = function.get_bound_args(func, args, kwargs) @@ -196,8 +196,7 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: if not isinstance(target, Member): log.trace("The target is not a discord.Member; skipping role hierarchy check.") - await func(*args, **kwargs) - return + return await func(*args, **kwargs) ctx = function.get_arg_value(1, bound_args) cmd = ctx.command.name @@ -214,7 +213,7 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: ) else: log.trace(f"{func.__name__}: {target.top_role=} < {actor.top_role=}; calling func") - await func(*args, **kwargs) + return await func(*args, **kwargs) return wrapper return decorator -- cgit v1.2.3 From 15fb882b49a2fb66b55b31aeb377ac03421de73d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:19:08 +0000 Subject: Change purgeban to use custom clean logic This migrates the purgeban command away from Discord's native purgeban to our custom logic. Discord's native purgeban does not leave us with any evidence or context of what messages were deleted. So when mods reference the infraction at a later date they are lacking information. Instead, we use our custom clean cog to delete all messages from the user in question for the last hour, and automatically append the link to the clean log to the infraction reason. . --- bot/exts/moderation/infraction/infractions.py | 70 ++++++++++++++++++++------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 7c0259b8e..20fcf28f9 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser +from bot.converters import Age, Duration, Expiry, Infraction, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -19,6 +19,11 @@ from bot.utils.messages import format_user log = get_logger(__name__) +if t.TYPE_CHECKING: + from bot.exts.moderation.clean import Clean + from bot.exts.moderation.infraction.management import ModManagement + from bot.exts.moderation.watchchannels.bigbrother import BigBrother + class Infractions(InfractionScheduler, commands.Cog): """Apply and pardon infractions on users for moderation purposes.""" @@ -101,11 +106,44 @@ class Infractions(InfractionScheduler, commands.Cog): reason: t.Optional[str] = None ) -> None: """ - Same as ban but removes all their messages of the last 24 hours. + Same as ban, but also cleans all their messages from the last hour. If duration is specified, it temporarily bans that user for the given duration. """ - await self.apply_ban(ctx, user, reason, 1, expires_at=duration) + clean_cog: t.Optional[Clean] = self.bot.get_cog("Clean") + if clean_cog is None: + # If we can't get the clean cog, fall back to native purgeban. + await self.apply_ban(ctx, user, reason, 1, expires_at=duration) + return + + infraction = await self.apply_ban(ctx, user, reason, expires_at=duration) + if not infraction or not infraction.get("id"): + # Ban was unsuccessful, quit early. + return + + # Calling commands directly skips Discord.py's convertors, so we need to convert args manually. + clean_time = await Age().convert(ctx, "1h") + infraction = await Infraction().convert(ctx, infraction["id"]) + + log_url = await clean_cog._clean_messages( + ctx, + users=[user], + channels="*", + first_limit=clean_time, + attempt_delete_invocation=False, + ) + + infr_manage_cog: t.Optional[ModManagement] = self.bot.get_cog("ModManagement") + if infr_manage_cog is None: + # If we can't get the mod management cog, don't bother appending the log. + return + + # Overwrite the context's send function so infraction append + # doesn't output the update infraction confirmation message. + async def send(*args, **kwargs) -> None: + pass + ctx.send = send + await infr_manage_cog.infraction_append(ctx, infraction, None, reason=f"[Clean log]({log_url})") @command(aliases=("vban",)) async def voiceban(self, ctx: Context) -> None: @@ -368,7 +406,7 @@ class Infractions(InfractionScheduler, commands.Cog): reason: t.Optional[str], purge_days: t.Optional[int] = 0, **kwargs - ) -> None: + ) -> t.Optional[dict]: """ Apply a ban infraction with kwargs passed to `post_infraction`. @@ -376,7 +414,7 @@ class Infractions(InfractionScheduler, commands.Cog): """ if isinstance(user, Member) and user.top_role >= ctx.me.top_role: await ctx.send(":x: I can't ban users above or equal to me in the role hierarchy.") - return + return None # In the case of a permanent ban, we don't need get_active_infractions to tell us if one is active is_temporary = kwargs.get("expires_at") is not None @@ -385,19 +423,19 @@ class Infractions(InfractionScheduler, commands.Cog): if active_infraction: if is_temporary: log.trace("Tempban ignored as it cannot overwrite an active ban.") - return + return None if active_infraction.get('expires_at') is None: log.trace("Permaban already exists, notify.") await ctx.send(f":x: User is already permanently banned (#{active_infraction['id']}).") - return + return None log.trace("Old tempban is being replaced by new permaban.") await self.pardon_infraction(ctx, "ban", user, send_msg=is_temporary) infraction = await _utils.post_infraction(ctx, user, "ban", reason, active=True, **kwargs) if infraction is None: - return + return None infraction["purge"] = "purge " if purge_days else "" @@ -409,19 +447,17 @@ class Infractions(InfractionScheduler, commands.Cog): action = ctx.guild.ban(user, reason=reason, delete_message_days=purge_days) await self.apply_infraction(ctx, infraction, user, action) + bb_cog: t.Optional[BigBrother] = self.bot.get_cog("Big Brother") if infraction.get('expires_at') is not None: log.trace(f"Ban isn't permanent; user {user} won't be unwatched by Big Brother.") - return - - bb_cog = self.bot.get_cog("Big Brother") - if not bb_cog: + elif not bb_cog: log.error(f"Big Brother cog not loaded; perma-banned user {user} won't be unwatched.") - return - - log.trace(f"Big Brother cog loaded; attempting to unwatch perma-banned user {user}.") + else: + log.trace(f"Big Brother cog loaded; attempting to unwatch perma-banned user {user}.") + bb_reason = "User has been permanently banned from the server. Automatically removed." + await bb_cog.apply_unwatch(ctx, user, bb_reason, send_message=False) - bb_reason = "User has been permanently banned from the server. Automatically removed." - await bb_cog.apply_unwatch(ctx, user, bb_reason, send_message=False) + return infraction @respect_role_hierarchy(member_arg=2) async def apply_voice_mute(self, ctx: Context, user: MemberOrUser, reason: t.Optional[str], **kwargs) -> None: -- cgit v1.2.3 From 954c1a963dc3917e02e0fb0ff4560131db8f20b4 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:56:28 +0000 Subject: Add more aliases to purgeban --- bot/exts/moderation/infraction/infractions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 20fcf28f9..e2c4c9ee4 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -96,8 +96,8 @@ class Infractions(InfractionScheduler, commands.Cog): """ await self.apply_ban(ctx, user, reason, expires_at=duration) - @command(aliases=('pban',)) - async def purgeban( + @command(aliases=("cban", "purgeban", "pban")) + async def cleanban( self, ctx: Context, user: UnambiguousMemberOrUser, -- cgit v1.2.3 From fdcdaac97f055285cee82354a9a1352dca002194 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 19:04:00 +0000 Subject: Don't append clean log if no clean was done from purge ban --- bot/exts/moderation/infraction/infractions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index e2c4c9ee4..32ff376cf 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -132,6 +132,9 @@ class Infractions(InfractionScheduler, commands.Cog): first_limit=clean_time, attempt_delete_invocation=False, ) + if not log_url: + # Cleaning failed, or there were no messages to clean, exit early. + return infr_manage_cog: t.Optional[ModManagement] = self.bot.get_cog("ModManagement") if infr_manage_cog is None: -- cgit v1.2.3 From 993529aa945a1f9ec8d769c770399dbe2cd8bd25 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 2 Jan 2022 20:13:53 +0000 Subject: Add tests for new CleanBan and Clean functionality --- .../exts/moderation/infraction/test_infractions.py | 90 +++++++++++++++++- tests/bot/exts/moderation/test_clean.py | 104 +++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 tests/bot/exts/moderation/test_clean.py diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index f89465f84..57235ec6d 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -1,13 +1,15 @@ import inspect import textwrap import unittest -from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch +from unittest.mock import ANY, AsyncMock, DEFAULT, MagicMock, Mock, patch from discord.errors import NotFound from bot.constants import Event +from bot.exts.moderation.clean import Clean from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction.infractions import Infractions +from bot.exts.moderation.infraction.management import ModManagement from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockUser, autospec @@ -231,3 +233,89 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): "DM": "**Failed**" }) notify_pardon_mock.assert_awaited_once() + + +class CleanBanTests(unittest.IsolatedAsyncioTestCase): + """Tests for cleanban functionality.""" + + def setUp(self): + self.bot = MockBot() + self.mod = MockMember(roles=[MockRole(id=7890123, position=10)]) + self.user = MockMember(roles=[MockRole(id=123456, position=1)]) + self.guild = MockGuild() + self.ctx = MockContext(bot=self.bot, author=self.mod) + self.cog = Infractions(self.bot) + self.clean_cog = Clean(self.bot) + self.management_cog = ModManagement(self.bot) + + self.cog.apply_ban = AsyncMock(return_value={"id": 42}) + self.log_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + self.clean_cog._clean_messages = AsyncMock(return_value=self.log_url) + + def mock_get_cog(self, enable_clean, enable_manage): + def inner(name): + if name == "ModManagement": + return self.management_cog if enable_manage else None + elif name == "Clean": + return self.clean_cog if enable_clean else None + else: + return DEFAULT + return inner + + async def test_cleanban_falls_back_to_native_purge_without_clean_cog(self): + """Should fallback to native purge if the Clean cog is not available.""" + self.bot.get_cog.side_effect = self.mock_get_cog(False, False) + + self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) + self.cog.apply_ban.assert_awaited_once_with( + self.ctx, + self.user, + "FooBar", + 1, + expires_at=None, + ) + + async def test_cleanban_doesnt_purge_messages_if_clean_cog_available(self): + """Cleanban command should use the native purge messages if the clean cog is available.""" + self.bot.get_cog.side_effect = self.mock_get_cog(True, False) + + self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) + self.cog.apply_ban.assert_awaited_once_with( + self.ctx, + self.user, + "FooBar", + expires_at=None, + ) + + @patch("bot.exts.moderation.infraction.infractions.Age") + async def test_cleanban_uses_clean_cog_when_available(self, mocked_age_converter): + """Test cleanban uses the clean cog to clean messages if it's available.""" + self.bot.api_client.patch = AsyncMock() + self.bot.get_cog.side_effect = self.mock_get_cog(True, False) + + mocked_age_converter.return_value.convert = AsyncMock(return_value="81M") + self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) + + self.clean_cog._clean_messages.assert_awaited_once_with( + self.ctx, + users=[self.user], + channels="*", + first_limit="81M", + attempt_delete_invocation=False, + ) + + @patch("bot.exts.moderation.infraction.infractions.Infraction") + async def test_cleanban_edits_infraction_reason(self, mocked_infraction_converter): + """Ensure cleanban edits the ban reason with a link to the clean log.""" + self.bot.get_cog.side_effect = self.mock_get_cog(True, True) + + self.management_cog.infraction_append = AsyncMock() + mocked_infraction_converter.return_value.convert = AsyncMock(return_value=42) + self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) + + self.management_cog.infraction_append.assert_awaited_once_with( + self.ctx, + 42, + None, + reason=f"[Clean log]({self.log_url})" + ) diff --git a/tests/bot/exts/moderation/test_clean.py b/tests/bot/exts/moderation/test_clean.py new file mode 100644 index 000000000..83489ea00 --- /dev/null +++ b/tests/bot/exts/moderation/test_clean.py @@ -0,0 +1,104 @@ +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from bot.exts.moderation.clean import Clean +from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockMessage, MockRole, MockTextChannel + + +class CleanTests(unittest.IsolatedAsyncioTestCase): + """Tests for clean cog functionality.""" + + def setUp(self): + self.bot = MockBot() + self.mod = MockMember(roles=[MockRole(id=7890123, position=10)]) + self.user = MockMember(roles=[MockRole(id=123456, position=1)]) + self.guild = MockGuild() + self.ctx = MockContext(bot=self.bot, author=self.mod) + self.cog = Clean(self.bot) + + self.log_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + self.cog._modlog_cleaned_messages = AsyncMock(return_value=self.log_url) + + self.cog._use_cache = MagicMock(return_value=True) + self.cog._delete_found = AsyncMock(return_value=[42, 84]) + + @patch("bot.exts.moderation.clean.is_mod_channel") + async def test_clean_deletes_invocation_in_non_mod_channel(self, mod_channel_check): + """Clean command should delete the invocation message if ran in a non mod channel.""" + mod_channel_check.return_value = False + self.ctx.message.delete = AsyncMock() + + self.assertIsNone(await self.cog._delete_invocation(self.ctx)) + + self.ctx.message.delete.assert_awaited_once() + + @patch("bot.exts.moderation.clean.is_mod_channel") + async def test_clean_doesnt_delete_invocation_in_mod_channel(self, mod_channel_check): + """Clean command should not delete the invocation message if ran in a mod channel.""" + mod_channel_check.return_value = True + self.ctx.message.delete = AsyncMock() + + self.assertIsNone(await self.cog._delete_invocation(self.ctx)) + + self.ctx.message.delete.assert_not_awaited() + + async def test_clean_doesnt_attempt_deletion_when_attempt_delete_invocation_is_false(self): + """Clean command should not attempt to delete the invocation message if attempt_delete_invocation is false.""" + self.cog._delete_invocation = AsyncMock() + self.bot.get_channel = MagicMock(return_value=False) + + self.assertEqual( + await self.cog._clean_messages( + self.ctx, + None, + first_limit=MockMessage(), + attempt_delete_invocation=False, + ), + self.log_url, + ) + + self.cog._delete_invocation.assert_not_awaited() + + @patch("bot.exts.moderation.clean.is_mod_channel") + async def test_clean_replies_with_success_message_when_ran_in_mod_channel(self, mod_channel_check): + """Clean command should reply to the message with a confirmation message if invoked in a mod channel.""" + mod_channel_check.return_value = True + self.ctx.reply = AsyncMock() + + self.assertEqual( + await self.cog._clean_messages( + self.ctx, + None, + first_limit=MockMessage(), + attempt_delete_invocation=False, + ), + self.log_url, + ) + + self.ctx.reply.assert_awaited_once() + sent_message = self.ctx.reply.await_args[0][0] + self.assertIn(self.log_url, sent_message) + self.assertIn("2 messages", sent_message) + + @patch("bot.exts.moderation.clean.is_mod_channel") + async def test_clean_send_success_message__to_mods_when_ran_in_non_mod_channel(self, mod_channel_check): + """Clean command should send a confirmation message to #mods if invoked in a non-mod channel.""" + mod_channel_check.return_value = False + mocked_mods = MockTextChannel(id=1234567) + mocked_mods.send = AsyncMock() + self.bot.get_channel = MagicMock(return_value=mocked_mods) + + self.assertEqual( + await self.cog._clean_messages( + self.ctx, + None, + first_limit=MockMessage(), + attempt_delete_invocation=False, + ), + self.log_url, + ) + + mocked_mods.send.assert_awaited_once() + sent_message = mocked_mods.send.await_args[0][0] + self.assertIn(self.log_url, sent_message) + self.assertIn("2 messages", sent_message) -- cgit v1.2.3 From 6c139905cca53f7810a100435955ec0c5fbc30e1 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 14 Feb 2022 01:51:23 +0000 Subject: Send error when cleanban fails to ban Co-authored-by: GDWR --- bot/exts/moderation/infraction/infractions.py | 4 +++- tests/bot/exts/moderation/infraction/test_infractions.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 32ff376cf..09ee1a7b4 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -113,12 +113,14 @@ class Infractions(InfractionScheduler, commands.Cog): clean_cog: t.Optional[Clean] = self.bot.get_cog("Clean") if clean_cog is None: # If we can't get the clean cog, fall back to native purgeban. - await self.apply_ban(ctx, user, reason, 1, expires_at=duration) + await self.apply_ban(ctx, user, reason, purge_days=1, expires_at=duration) return infraction = await self.apply_ban(ctx, user, reason, expires_at=duration) if not infraction or not infraction.get("id"): # Ban was unsuccessful, quit early. + await ctx.send(":x: Failed to apply ban.") + log.error("Failed to apply ban to user %d", user.id) return # Calling commands directly skips Discord.py's convertors, so we need to convert args manually. diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 57235ec6d..8845fb382 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -271,7 +271,7 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): self.ctx, self.user, "FooBar", - 1, + purge_days=1, expires_at=None, ) -- cgit v1.2.3 From 762b107056145d44b5219a929302455c9e6ed1d0 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 14 Feb 2022 01:53:33 +0000 Subject: Typo and docstrings in clean ban tests Co-authored-by: GDWR --- tests/bot/exts/moderation/infraction/test_infractions.py | 1 + tests/bot/exts/moderation/test_clean.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 8845fb382..8bed1e386 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -253,6 +253,7 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): self.clean_cog._clean_messages = AsyncMock(return_value=self.log_url) def mock_get_cog(self, enable_clean, enable_manage): + """Mock get cog factory that allows the user to specify whether clean and manage cogs are enabled.""" def inner(name): if name == "ModManagement": return self.management_cog if enable_manage else None diff --git a/tests/bot/exts/moderation/test_clean.py b/tests/bot/exts/moderation/test_clean.py index 83489ea00..d7647fa48 100644 --- a/tests/bot/exts/moderation/test_clean.py +++ b/tests/bot/exts/moderation/test_clean.py @@ -81,7 +81,7 @@ class CleanTests(unittest.IsolatedAsyncioTestCase): self.assertIn("2 messages", sent_message) @patch("bot.exts.moderation.clean.is_mod_channel") - async def test_clean_send_success_message__to_mods_when_ran_in_non_mod_channel(self, mod_channel_check): + async def test_clean_send_success_message_to_mods_when_ran_in_non_mod_channel(self, mod_channel_check): """Clean command should send a confirmation message to #mods if invoked in a non-mod channel.""" mod_channel_check.return_value = False mocked_mods = MockTextChannel(id=1234567) -- cgit v1.2.3 From 7e8e95f07e343f1d7d9a8069b6cdb1a9fcbb00d7 Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Wed, 16 Feb 2022 22:48:09 +0000 Subject: Remove unnecessary Infraction conversion in clean ban (#2092) --- bot/exts/moderation/infraction/infractions.py | 3 +-- tests/bot/exts/moderation/infraction/test_infractions.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 09ee1a7b4..af42ab1b8 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Age, Duration, Expiry, Infraction, MemberOrUser, UnambiguousMemberOrUser +from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -125,7 +125,6 @@ class Infractions(InfractionScheduler, commands.Cog): # Calling commands directly skips Discord.py's convertors, so we need to convert args manually. clean_time = await Age().convert(ctx, "1h") - infraction = await Infraction().convert(ctx, infraction["id"]) log_url = await clean_cog._clean_messages( ctx, diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 8bed1e386..052048053 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -305,18 +305,16 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): attempt_delete_invocation=False, ) - @patch("bot.exts.moderation.infraction.infractions.Infraction") - async def test_cleanban_edits_infraction_reason(self, mocked_infraction_converter): + async def test_cleanban_edits_infraction_reason(self): """Ensure cleanban edits the ban reason with a link to the clean log.""" self.bot.get_cog.side_effect = self.mock_get_cog(True, True) self.management_cog.infraction_append = AsyncMock() - mocked_infraction_converter.return_value.convert = AsyncMock(return_value=42) self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) self.management_cog.infraction_append.assert_awaited_once_with( self.ctx, - 42, + {"id": 42}, None, reason=f"[Clean log]({self.log_url})" ) -- cgit v1.2.3 From abfb0ed9f0b3f1c676dc75707e51b33ded86cdf5 Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 17 Feb 2022 11:01:53 +0000 Subject: Validate regex when adding to the filter_token filter --- bot/exts/filters/filter_lists.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bot/exts/filters/filter_lists.py b/bot/exts/filters/filter_lists.py index ee5bd89f3..d3e6393d3 100644 --- a/bot/exts/filters/filter_lists.py +++ b/bot/exts/filters/filter_lists.py @@ -1,3 +1,4 @@ +import re from typing import Optional from discord import Colour, Embed @@ -72,6 +73,18 @@ class FilterLists(Cog): elif list_type == "FILE_FORMAT" and not content.startswith("."): content = f".{content}" + # If it's a filter token, validate the passed regex + elif list_type == "FILTER_TOKEN": + try: + _ = re.compile(content) + except re.error: + await ctx.message.add_reaction("❌") + await ctx.send( + f"{ctx.author.mention} that's not a valid regex! " + "You may have forgotten to escape part of the regex." + ) + return + # Try to add the item to the database log.trace(f"Trying to add the {content} item to the {list_type} {allow_type}") payload = { -- cgit v1.2.3 From b0e21f0a5f342cd8c9e82ddc357b51e19c3fc9ad Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 17 Feb 2022 11:47:29 +0000 Subject: Include regex error in failure message f --- bot/exts/filters/filter_lists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/filters/filter_lists.py b/bot/exts/filters/filter_lists.py index d3e6393d3..9d3a52942 100644 --- a/bot/exts/filters/filter_lists.py +++ b/bot/exts/filters/filter_lists.py @@ -77,11 +77,11 @@ class FilterLists(Cog): elif list_type == "FILTER_TOKEN": try: _ = re.compile(content) - except re.error: + except re.error as e: await ctx.message.add_reaction("❌") await ctx.send( f"{ctx.author.mention} that's not a valid regex! " - "You may have forgotten to escape part of the regex." + f"Regex error message: {e.msg}." ) return -- cgit v1.2.3 From 97ddd3a709e1a6b7e821a438b61cd7b64a76ba62 Mon Sep 17 00:00:00 2001 From: minalike Date: Thu, 17 Feb 2022 21:40:16 -0500 Subject: Add user ID to message content for all mod alerts This is a temporary quality of life improvement until filters rewrite. Largely benefits mobile moderators who cannot copy from embeds. --- bot/exts/filters/filtering.py | 1 + bot/exts/moderation/modlog.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 1f83acf9b..9d491baa5 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -439,6 +439,7 @@ class Filtering(Cog): # Send pretty mod log embed to mod-alerts await self.mod_log.send_log_message( + content=str(msg.author.id), # quality-of-life improvement for mobile moderators to copy & paste icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title=f"{_filter['type'].title()} triggered!", diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 54a08738c..32ea0dc6a 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -116,7 +116,7 @@ class ModLog(Cog, name="ModLog"): if ping_everyone: if content: - content = f"<@&{Roles.moderators}>\n{content}" + content = f"<@&{Roles.moderators}> {content}" else: content = f"<@&{Roles.moderators}>" -- cgit v1.2.3 From 7c8458c8a1aa6443c31bcd319c439b86c7e1c384 Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 18 Feb 2022 10:37:18 +0000 Subject: Remove auto joining of new threads. --- bot/exts/utils/bot.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/bot/exts/utils/bot.py b/bot/exts/utils/bot.py index 788692777..8f0094bc9 100644 --- a/bot/exts/utils/bot.py +++ b/bot/exts/utils/bot.py @@ -1,7 +1,6 @@ -from contextlib import suppress from typing import Optional -from discord import Embed, Forbidden, TextChannel, Thread +from discord import Embed, TextChannel from discord.ext.commands import Cog, Context, command, group, has_any_role from bot.bot import Bot @@ -17,20 +16,6 @@ class BotCog(Cog, name="Bot"): def __init__(self, bot: Bot): self.bot = bot - @Cog.listener() - async def on_thread_join(self, thread: Thread) -> None: - """ - Try to join newly created threads. - - Despite the event name being misleading, this is dispatched when new threads are created. - """ - if thread.me: - # We have already joined this thread - return - - with suppress(Forbidden): - await thread.join() - @group(invoke_without_command=True, name="bot", hidden=True) async def botinfo_group(self, ctx: Context) -> None: """Bot informational commands.""" -- cgit v1.2.3 From f500249dfe8c8a0e1a957640c85c5e23d2af335e Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Fri, 18 Feb 2022 13:10:10 +0000 Subject: Remove unnecessary assignment Co-authored-by: ChrisJL --- bot/exts/filters/filter_lists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/filter_lists.py b/bot/exts/filters/filter_lists.py index 9d3a52942..a883ddf54 100644 --- a/bot/exts/filters/filter_lists.py +++ b/bot/exts/filters/filter_lists.py @@ -76,7 +76,7 @@ class FilterLists(Cog): # If it's a filter token, validate the passed regex elif list_type == "FILTER_TOKEN": try: - _ = re.compile(content) + re.compile(content) except re.error as e: await ctx.message.add_reaction("❌") await ctx.send( -- cgit v1.2.3 From 8283043d1b60be3f7ad9094983c2bf1b959fb70b Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 13 Feb 2022 21:26:47 +0000 Subject: Add a cog to bump threads Quite often we want threads such as event discussions, or moderation discussions to live beyond their maximum of 1 week of auto-archival. This cog allows staff to add a thread to a list that will get 'bumped' back open by the bot when they are auto-archived --- bot/exts/utils/thread_bumper.py | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 bot/exts/utils/thread_bumper.py diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py new file mode 100644 index 000000000..a10d151aa --- /dev/null +++ b/bot/exts/utils/thread_bumper.py @@ -0,0 +1,114 @@ +import typing as t + +import discord +from async_rediscache import RedisCache +from discord.ext import commands + +from bot import constants +from bot.bot import Bot +from bot.log import get_logger +from bot.pagination import LinePaginator +from bot.utils import scheduling + +log = get_logger(__name__) + + +class ThreadBumper(commands.Cog): + """Cog that allow users to add the current thread to a list that get reopened on archive.""" + + # RedisCache[discord.Thread.id, "sentinel"] + threads_to_bump = RedisCache() + + def __init__(self, bot: Bot): + self.bot = bot + self.init_task = scheduling.create_task(self.ensure_bumped_threads_are_active(), event_loop=self.bot.loop) + + async def ensure_bumped_threads_are_active(self) -> None: + """Ensure bumped threads are active, since threads could have been archived while the bot was down.""" + await self.bot.wait_until_guild_available() + + for thread_id, _ in await self.threads_to_bump.items(): + if thread := self.bot.get_channel(thread_id): + if not thread.archived: + continue + + try: + thread = await self.bot.fetch_channel(thread_id) + except discord.NotFound: + log.info(f"Thread {thread_id} has been deleted, removing from bumped threads.") + await self.threads_to_bump.delete(thread_id) + if thread.archived: + await thread.edit(archived=False) + + @commands.group(name="bump") + async def thread_bump_group(self, ctx: commands.Context) -> None: + """A group of commands to manage the bumping of threads.""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @thread_bump_group.command(name="add") + async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: + """Add a thread to the bump list.""" + await self.init_task + + if not thread: + if isinstance(ctx.channel, discord.Thread): + thread = ctx.channel + else: + raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + + await self.threads_to_bump.set(thread.id, "sentinel") + await ctx.send(f":ok_hand:{thread.mention} has been added to the bump list.") + + @thread_bump_group.command(name="remove", aliases=("r", "rem", "d", "del", "delete")) + async def remove_thread_from_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: + """Remove a thread from the bump list.""" + await self.init_task + + if not thread: + if isinstance(ctx.channel, discord.Thread): + thread = ctx.channel + else: + raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + + await self.threads_to_bump.delete(thread.id) + await ctx.send(f":ok_hand: {thread.mention} has been removed from the bump list.") + + @thread_bump_group.command(name="list", aliases=("get",)) + async def list_all_threads_in_bump_list(self, ctx: commands.Context) -> None: + """List all the threads in the bump list.""" + await self.init_task + + lines = [f"<#{k}>" for k, _ in await self.threads_to_bump.items()] + embed = discord.Embed( + title="Threads in the bump list", + colour=constants.Colours.blue + ) + await LinePaginator.paginate(lines, ctx, embed) + + @commands.Cog.listener() + async def on_thread_update(self, _: discord.Thread, after: discord.Thread) -> None: + """ + Listen for thread updates and check if the thread has been archived. + + If the thread has been archived, and is in the bump list, un-archive it. + """ + await self.init_task + + if not after.archived: + return + + bumped_threads = [k for k, _ in await self.threads_to_bump.items()] + if after.id in bumped_threads: + await after.edit(archived=False) + + async def cog_check(self, ctx: commands.Context) -> bool: + """Only allow staff & partner roles to invoke the commands in this cog.""" + return await commands.has_any_role( + *constants.STAFF_PARTNERS_COMMUNITY_ROLES + ).predicate(ctx) + + +def setup(bot: Bot) -> None: + """Load the ThreadBumper cog.""" + bot.add_cog(ThreadBumper(bot)) -- cgit v1.2.3 From abdfd0db7caa2961428e6cf6b601df4aaccd9151 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 17 Feb 2022 01:23:56 +0000 Subject: Add logic so that manually archived threads bypass the thread bump list --- bot/exts/utils/thread_bumper.py | 46 +++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index a10d151aa..8c6f3518e 100644 --- a/bot/exts/utils/thread_bumper.py +++ b/bot/exts/utils/thread_bumper.py @@ -8,7 +8,7 @@ from bot import constants from bot.bot import Bot from bot.log import get_logger from bot.pagination import LinePaginator -from bot.utils import scheduling +from bot.utils import channel, scheduling log = get_logger(__name__) @@ -23,22 +23,50 @@ class ThreadBumper(commands.Cog): self.bot = bot self.init_task = scheduling.create_task(self.ensure_bumped_threads_are_active(), event_loop=self.bot.loop) + async def unarchive_threads_not_manually_archived(self, threads: list[discord.Thread]) -> None: + """ + Iterate through and unarchive any threads that weren't manually archived recently. + + This is done by extracting the manually archived threads from the audit log. + + Only the last 200 thread_update logs are checked, + as this is assumed to be more than enough to cover bot downtime. + """ + guild = self.bot.get_guild(constants.Guild.id) + + recent_manually_archived_thread_ids = [] + async for thread_update in guild.audit_logs(limit=200, action=discord.AuditLogAction.thread_update): + if getattr(thread_update.after, "archived", False): + recent_manually_archived_thread_ids.append(thread_update.target.id) + + for thread in threads: + if thread.id in recent_manually_archived_thread_ids: + log.info( + "#%s (%d) was manually archived. Leaving archived, and removing from bumped threads.", + thread.name, + thread.id + ) + await self.threads_to_bump.delete(thread.id) + else: + await thread.edit(archived=False) + async def ensure_bumped_threads_are_active(self) -> None: """Ensure bumped threads are active, since threads could have been archived while the bot was down.""" await self.bot.wait_until_guild_available() + threads_to_maybe_bump = [] for thread_id, _ in await self.threads_to_bump.items(): - if thread := self.bot.get_channel(thread_id): - if not thread.archived: - continue - try: - thread = await self.bot.fetch_channel(thread_id) + thread = await channel.get_or_fetch_channel(thread_id) except discord.NotFound: - log.info(f"Thread {thread_id} has been deleted, removing from bumped threads.") + log.info("Thread %d has been deleted, removing from bumped threads.", thread_id) await self.threads_to_bump.delete(thread_id) + continue + if thread.archived: - await thread.edit(archived=False) + threads_to_maybe_bump.append(thread) + + await self.unarchive_threads_not_manually_archived(threads_to_maybe_bump) @commands.group(name="bump") async def thread_bump_group(self, ctx: commands.Context) -> None: @@ -100,7 +128,7 @@ class ThreadBumper(commands.Cog): bumped_threads = [k for k, _ in await self.threads_to_bump.items()] if after.id in bumped_threads: - await after.edit(archived=False) + await self.unarchive_threads_not_manually_archived([after]) async def cog_check(self, ctx: commands.Context) -> bool: """Only allow staff & partner roles to invoke the commands in this cog.""" -- cgit v1.2.3 From 3346d71416fdb3223d0c4998f92e420886445fac Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 18 Feb 2022 17:13:31 +0000 Subject: fixup: implemeent code review comments --- bot/exts/utils/thread_bumper.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index 8c6f3518e..35057f1fe 100644 --- a/bot/exts/utils/thread_bumper.py +++ b/bot/exts/utils/thread_bumper.py @@ -74,7 +74,7 @@ class ThreadBumper(commands.Cog): if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) - @thread_bump_group.command(name="add") + @thread_bump_group.command(name="add", aliases=("a",)) async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: """Add a thread to the bump list.""" await self.init_task @@ -85,6 +85,9 @@ class ThreadBumper(commands.Cog): else: raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + if await self.threads_to_bump.contains(thread.id): + raise commands.BadArgument("This thread is already in the bump list.") + await self.threads_to_bump.set(thread.id, "sentinel") await ctx.send(f":ok_hand:{thread.mention} has been added to the bump list.") @@ -99,6 +102,9 @@ class ThreadBumper(commands.Cog): else: raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + if not await self.threads_to_bump.contains(thread.id): + raise commands.BadArgument("This thread is not in the bump list.") + await self.threads_to_bump.delete(thread.id) await ctx.send(f":ok_hand: {thread.mention} has been removed from the bump list.") @@ -126,8 +132,7 @@ class ThreadBumper(commands.Cog): if not after.archived: return - bumped_threads = [k for k, _ in await self.threads_to_bump.items()] - if after.id in bumped_threads: + if await self.threads_to_bump.contains(after.id): await self.unarchive_threads_not_manually_archived([after]) async def cog_check(self, ctx: commands.Context) -> bool: -- cgit v1.2.3 From 7eea60288866322836956a7b3689e49a3e9c5c41 Mon Sep 17 00:00:00 2001 From: mina Date: Fri, 18 Feb 2022 17:22:16 -0500 Subject: Add user ID in message content for mod-alerts, but not for autobans --- bot/exts/filters/antispam.py | 1 + bot/exts/filters/filtering.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index ddfd11231..bcd845a43 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -103,6 +103,7 @@ class DeletionContext: mod_alert_message += content await modlog.send_log_message( + content=", ".join(str(m.id) for m in self.members), # quality-of-life improvement for mobile moderators icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title="Spam detected!", diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 9d491baa5..f44b28125 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -256,6 +256,7 @@ class Filtering(Cog): ) await self.mod_log.send_log_message( + content=str(member.id), # quality-of-life improvement for mobile moderators icon_url=Icons.token_removed, colour=Colours.soft_red, title="Username filtering alert", @@ -423,9 +424,12 @@ class Filtering(Cog): # Allow specific filters to override ping_everyone ping_everyone = Filter.ping_everyone and _filter.get("ping_everyone", True) - # If we are going to autoban, we don't want to ping + content = str(msg.author.id) # quality-of-life improvement for mobile moderators + + # If we are going to autoban, we don't want to ping and don't need the user ID if reason and "[autoban]" in reason: ping_everyone = False + content = None eval_msg = "using !eval " if is_eval else "" footer = f"Reason: {reason}" if reason else None @@ -439,7 +443,7 @@ class Filtering(Cog): # Send pretty mod log embed to mod-alerts await self.mod_log.send_log_message( - content=str(msg.author.id), # quality-of-life improvement for mobile moderators to copy & paste + content=content, icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title=f"{_filter['type'].title()} triggered!", -- cgit v1.2.3 From d258203483e1c7d4d6044dbcc3b1628c8bc3a319 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Sun, 20 Feb 2022 19:55:41 +0000 Subject: Remove discord formatted timestamp from log message (#2100) --- bot/exts/moderation/stream.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 4dccc8a7e..985cc6eb1 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -133,8 +133,12 @@ class Stream(commands.Cog): await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {time.discord_timestamp(duration)}.") # Convert here for nicer logging - revoke_time = time.format_with_duration(duration) - log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.") + humanized_duration = time.humanize_delta(duration, arrow.utcnow(), max_units=2) + end_time = duration.strftime("%Y-%m-%d %H:%M:%S") + log.debug( + f"Successfully gave {member} ({member.id}) permission " + f"to stream for {humanized_duration} (until {end_time})." + ) @commands.command(aliases=("pstream",)) @commands.has_any_role(*MODERATION_ROLES) -- cgit v1.2.3 From 33203cbfe95f28501af28a42e0c49f4d42b6f021 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 9 Feb 2022 10:38:13 +0000 Subject: Cancel help channel claim on 500 from Discord If we get a 500 error from Discord when trying to move the help channel to in use, attempt to let the user know, then cancel the claim. --- bot/exts/help_channels/_cog.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 541c791e5..6d061c8ca 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -111,14 +111,31 @@ class HelpChannels(commands.Cog): """ log.info(f"Channel #{message.channel} was claimed by `{message.author.id}`.") + try: + await self.move_to_in_use(message.channel) + except discord.DiscordServerError: + try: + await message.channel.send( + "The bot encountered a Discord API error while trying to move this channel, please try again later." + ) + except Exception as e: + log.warning("Error occurred while sending fail claim message:", exc_info=e) + log.info( + "500 error from Discord when moving #%s (%d) to in-use for %s (%d). Cancelling claim.", + message.channel.name, + message.channel.id, + message.author.name, + message.author.id, + ) + self.bot.stats.incr("help.failed_claims.500_on_move") + return + embed = discord.Embed( description=f"Channel claimed by {message.author.mention}.", color=constants.Colours.bright_green, ) await message.channel.send(embed=embed) - await self.move_to_in_use(message.channel) - # Handle odd edge case of `message.author` not being a `discord.Member` (see bot#1839) if not isinstance(message.author, discord.Member): log.debug(f"{message.author} ({message.author.id}) isn't a member. Not giving cooldown role or sending DM.") -- cgit v1.2.3 From 6505ab00f0695148e0e52ed1dc6815b37d4b6cae Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 9 Feb 2022 12:55:55 +0000 Subject: Ensure each in-use channel has a cached claimant on init This avoids issues when a user tries to close a channel, but the cache is empty, so the author check fails. --- bot/exts/help_channels/_channel.py | 34 ++++++++++++++++++++++++++++++++++ bot/exts/help_channels/_cog.py | 1 + 2 files changed, 35 insertions(+) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index e43c1e789..ff9e6a347 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,3 +1,4 @@ +import re import typing as t from datetime import timedelta from enum import Enum @@ -16,6 +17,7 @@ log = get_logger(__name__) MAX_CHANNELS_PER_CATEGORY = 50 EXCLUDED_CHANNELS = (constants.Channels.cooldown,) +CLAIMED_BY_RE = re.compile(r"Channel claimed by <@!?(?P\d{17,20})>\.$") class ClosingReason(Enum): @@ -157,3 +159,35 @@ async def move_to_bottom(channel: discord.TextChannel, category_id: int, **optio # Now that the channel is moved, we can edit the other attributes if options: await channel.edit(**options) + + +async def ensure_cached_claimant(channel: discord.TextChannel) -> None: + """ + Ensure there is a claimant cached for each help channel. + + Check the redis cache first, return early if there is already a claimant cached. + If there isn't an entry in redis, search for the "Claimed by X." embed in channel history. + Stopping early if we discover a dormant message first. + + If a claimant could not be found, send a warning to #helpers and set the claimant to the bot. + """ + if await _caches.claimants.get(channel.id): + return + + async for message in channel.history(limit=1000): + if message.author.id != bot.instance.user.id: + # We only care about bot messages + continue + if message.embeds: + if _message._match_bot_embed(message, _message.DORMANT_MSG): + log.info("Hit the dormant message embed before finding a claimant in %s (%d).", channel, channel.id) + break + user_id = CLAIMED_BY_RE.match(message.embeds[0].description).group("user_id") + await _caches.claimants.set(channel.id, int(user_id)) + return + + await bot.instance.get_channel(constants.Channels.helpers).send( + f"I couldn't find a claimant for {channel.mention} in that last 1000 messages. " + "Please use your helper powers to close the channel if/when appropriate." + ) + await _caches.claimants.set(channel.id, bot.instance.user.id) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 6d061c8ca..b0f1a1dce 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -326,6 +326,7 @@ class HelpChannels(commands.Cog): log.trace("Moving or rescheduling in-use channels.") for channel in _channel.get_category_channels(self.in_use_category): + await _channel.ensure_cached_claimant(channel) await self.move_idle_channel(channel, has_task=False) # Prevent the command from being used until ready. -- cgit v1.2.3 From 4e0c6e73a5d430574ec73257734c78a8c6574fa2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 20 Feb 2022 14:10:49 +0000 Subject: Handle uncached claimant on unclaim This could be possible during init_available. If there are too many available channels they are made dormant by calling unclaim_channel. However there may not be claimants cached and ensure_claimants wouldn't populate cache, since the channels weren't in use. --- bot/exts/help_channels/_cog.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index b0f1a1dce..f276a7993 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -452,18 +452,21 @@ class HelpChannels(commands.Cog): async def _unclaim_channel( self, channel: discord.TextChannel, - claimant_id: int, + claimant_id: t.Optional[int], closed_on: _channel.ClosingReason ) -> None: """Actual implementation of `unclaim_channel`. See that for full documentation.""" await _caches.claimants.delete(channel.id) await _caches.session_participants.delete(channel.id) - claimant = await members.get_or_fetch_member(self.guild, claimant_id) - if claimant is None: - log.info(f"{claimant_id} left the guild during their help session; the cooldown role won't be removed") + if not claimant_id: + log.info("No claimant given when un-claiming %s (%d). Skipping role removal.", channel, channel.id) else: - await members.handle_role_change(claimant, claimant.remove_roles, self.cooldown_role) + claimant = await members.get_or_fetch_member(self.guild, claimant_id) + if claimant is None: + log.info(f"{claimant_id} left the guild during their help session; the cooldown role won't be removed") + else: + await members.handle_role_change(claimant, claimant.remove_roles, self.cooldown_role) await _message.unpin(channel) await _stats.report_complete_session(channel.id, closed_on) -- cgit v1.2.3 From 75b3a515910f04f295e0b70c208e8383b1783b7d Mon Sep 17 00:00:00 2001 From: minalike Date: Tue, 22 Feb 2022 19:30:20 -0500 Subject: πŸ‘Œ Fix indentation and update grammar for when only 1 channel remains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 17 ++++++++++------- config-default.yml | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index f8f10f774..39132b0f1 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -128,8 +128,8 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. - If a notification was sent, return the time at which the message was sent. - Otherwise, return None. + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications @@ -175,7 +175,7 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar This will include the number of dormant channels left `number_of_channels_left` If a notification was sent, return the time at which the message was sent. - Otherwise, return None. + Otherwise, return None. Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications @@ -200,10 +200,13 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar log.trace("Did not send notify_running notification as the notification channel couldn't be gathered.") try: - await channel.send( - f"There are only {number_of_channels_left} dormant channels left. " - "Consider participating in some help channels so that we don't run out." - ) + if number_of_channels_left == 1: + message = f"There is only {number_of_channels_left} dormant channel left. " + else: + message = f"There are only {number_of_channels_left} dormant channels left. " + message += "Consider participating in some help channels so that we don't run out." + await channel.send(message) + except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about running low of dormant channels!") diff --git a/config-default.yml b/config-default.yml index 6ad471cbd..dae923158 100644 --- a/config-default.yml +++ b/config-default.yml @@ -517,7 +517,7 @@ help_channels: notify_minutes: 15 # Minimum interval between none_remaining or running_low notifications notify_none_remaining: true # Pinging notification for the Helper role when no dormant channels remain - notify_none_remaining_roles: # Mention these roles in the non_remaining notification + notify_none_remaining_roles: # Mention these roles in the none_remaining notification - *HELPERS_ROLE notify_running_low: true # Non-pinging notification which is triggered when the channel count is equal or less than the threshold -- cgit v1.2.3 From 54480a1a166511c8b85dbb46403247144138fd24 Mon Sep 17 00:00:00 2001 From: minalike Date: Tue, 22 Feb 2022 19:45:34 -0500 Subject: πŸ‘Œ Only send metric if helpers were notified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 39132b0f1..6e986282e 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -162,10 +162,9 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") - finally: + else: bot.instance.stats.incr("help.out_of_channel_alerts") - - return arrow.utcnow() + return arrow.utcnow() async def notify_running_low(number_of_channels_left: int, last_notification: Arrow) -> t.Optional[Arrow]: @@ -210,10 +209,9 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about running low of dormant channels!") - finally: + else: bot.instance.stats.incr("help.running_low_alerts") - - return arrow.utcnow() + return arrow.utcnow() async def pin(message: discord.Message) -> None: -- cgit v1.2.3 From 6c7444d6f61fe201f97673de435a63d4b4f67c7d Mon Sep 17 00:00:00 2001 From: minalike Date: Tue, 22 Feb 2022 20:08:29 -0500 Subject: πŸ› Fix to correctly calculate number of seconds from last notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit total_seconds() is the correct method to obtain a time delta in seconds --- bot/exts/help_channels/_message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 6e986282e..0aabb9bfb 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -139,7 +139,7 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: if not constants.HelpChannels.notify_none_remaining: return None - if (arrow.utcnow() - last_notification).seconds < (constants.HelpChannels.notify_minutes * 60): + if (arrow.utcnow() - last_notification).total_seconds() < (constants.HelpChannels.notify_minutes * 60): log.trace("Did not send none_remaining notification as it hasn't been enough time since the last one.") return None @@ -188,7 +188,7 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar log.trace("Did not send notify_running_low notification as the threshold was not met.") return None - if (arrow.utcnow() - last_notification).seconds < (constants.HelpChannels.notify_minutes * 60): + if (arrow.utcnow() - last_notification).total_seconds() < (constants.HelpChannels.notify_minutes * 60): log.trace("Did not send notify_running_low notification as it hasn't been enough time since the last one.") return None -- cgit v1.2.3 From 01e877947a1a0e73d6197cdb7af5a9dd03387337 Mon Sep 17 00:00:00 2001 From: minalike Date: Tue, 22 Feb 2022 20:42:01 -0500 Subject: Fixup: remove extra blank line --- bot/exts/help_channels/_message.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 0aabb9bfb..7ceed9b4d 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -205,7 +205,6 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar message = f"There are only {number_of_channels_left} dormant channels left. " message += "Consider participating in some help channels so that we don't run out." await channel.send(message) - except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about running low of dormant channels!") -- cgit v1.2.3 From 9044f5404ed2be1b5eb4160f6d2a86e329f6abf5 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Wed, 23 Feb 2022 07:30:28 -0500 Subject: fix: Make sure the regex match is not None before adding to claimaints cache If there was a bot message in a help channel that contained an embed that was not the claimed channel message, this would raise an attribute error. --- bot/exts/help_channels/_channel.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index ff9e6a347..ea7d972b5 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -182,9 +182,10 @@ async def ensure_cached_claimant(channel: discord.TextChannel) -> None: if _message._match_bot_embed(message, _message.DORMANT_MSG): log.info("Hit the dormant message embed before finding a claimant in %s (%d).", channel, channel.id) break - user_id = CLAIMED_BY_RE.match(message.embeds[0].description).group("user_id") - await _caches.claimants.set(channel.id, int(user_id)) - return + # Only set the claimant if the first embed matches the claimed channel embed regex + if match := CLAIMED_BY_RE.match(message.embeds[0].description): + await _caches.claimants.set(channel.id, int(match.group("user_id"))) + return await bot.instance.get_channel(constants.Channels.helpers).send( f"I couldn't find a claimant for {channel.mention} in that last 1000 messages. " -- cgit v1.2.3 From b67ee61ada08ebcd89c07f29a36ae5d0ba2ae057 Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 28 Feb 2022 19:47:57 -0500 Subject: fix: Make help buttons only work for author --- bot/exts/info/help.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 06799fb71..ad784c87b 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -113,12 +113,22 @@ class CommandView(ui.View): If the command has a parent, a button is added to the view to show that parent's help embed. """ - def __init__(self, help_command: CustomHelpCommand, command: Command): + def __init__(self, help_command: CustomHelpCommand, command: Command, context: Context): + self.context = context super().__init__() if command.parent: self.children.append(GroupButton(help_command, command, emoji="↩️")) + async def interaction_check(self, interaction: Interaction) -> bool: + """Ensures the button only works for the user who spawned the help command.""" + if interaction.user is not None: + + if interaction.user.id == self.context.author.id: + return True + + return False + class GroupView(CommandView): """ @@ -130,8 +140,8 @@ class GroupView(CommandView): MAX_BUTTONS_IN_ROW = 5 MAX_ROWS = 5 - def __init__(self, help_command: CustomHelpCommand, group: Group, subcommands: list[Command]): - super().__init__(help_command, group) + def __init__(self, help_command: CustomHelpCommand, group: Group, subcommands: list[Command], context: Context): + super().__init__(help_command, group, context) # Don't add buttons if only a portion of the subcommands can be shown. if len(subcommands) + len(self.children) > self.MAX_ROWS * self.MAX_BUTTONS_IN_ROW: log.trace(f"Attempted to add navigation buttons for `{group.qualified_name}`, but there was no space.") @@ -302,7 +312,7 @@ class CustomHelpCommand(HelpCommand): embed.description = command_details # If the help is invoked in the context of an error, don't show subcommand navigation. - view = CommandView(self, command) if not self.context.command_failed else None + view = CommandView(self, command, self.context) if not self.context.command_failed else None return embed, view async def send_command_help(self, command: Command) -> None: @@ -347,7 +357,7 @@ class CustomHelpCommand(HelpCommand): embed.description += f"\n**Subcommands:**\n{command_details}" # If the help is invoked in the context of an error, don't show subcommand navigation. - view = GroupView(self, group, commands_) if not self.context.command_failed else None + view = GroupView(self, group, commands_, self.context) if not self.context.command_failed else None return embed, view async def send_group_help(self, group: Group) -> None: -- cgit v1.2.3 From 7637f1c139ba7df50ca6ac56192aef769355cc80 Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 28 Feb 2022 19:53:35 -0500 Subject: docs: Make docstring sound better --- bot/exts/info/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index ad784c87b..33ce40c88 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -121,7 +121,7 @@ class CommandView(ui.View): self.children.append(GroupButton(help_command, command, emoji="↩️")) async def interaction_check(self, interaction: Interaction) -> bool: - """Ensures the button only works for the user who spawned the help command.""" + """Ensures that the button only works for the user who spawned the help command.""" if interaction.user is not None: if interaction.user.id == self.context.author.id: -- cgit v1.2.3 From 61e2bb4a90202d12c36754e6936b33fbdebb2f7d Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 28 Feb 2022 22:51:21 -0500 Subject: feat: Allow moderators to use buttons in other people's help command --- bot/exts/info/help.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 33ce40c88..71f244eac 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -123,8 +123,10 @@ class CommandView(ui.View): async def interaction_check(self, interaction: Interaction) -> bool: """Ensures that the button only works for the user who spawned the help command.""" if interaction.user is not None: + if any(role.id in constants.MODERATION_ROLES for role in interaction.user.roles): + return True - if interaction.user.id == self.context.author.id: + elif interaction.user.id == self.context.author.id: return True return False -- cgit v1.2.3 From 9e204117dfd236c0cc2fb8355899df2feab63d29 Mon Sep 17 00:00:00 2001 From: an-dyy Date: Tue, 1 Mar 2022 15:24:28 -0500 Subject: docs: Added docstring for moderator access --- bot/exts/info/help.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 71f244eac..864e7edd2 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -121,7 +121,11 @@ class CommandView(ui.View): self.children.append(GroupButton(help_command, command, emoji="↩️")) async def interaction_check(self, interaction: Interaction) -> bool: - """Ensures that the button only works for the user who spawned the help command.""" + """ + Ensures that the button only works for the user who spawned the help command. + + Also allows moderators to access buttons even when not the author of message. + """ if interaction.user is not None: if any(role.id in constants.MODERATION_ROLES for role in interaction.user.roles): return True -- cgit v1.2.3