From c156036a8d80936e8ae8712e689a82acd821e986 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Mon, 15 Jul 2024 00:59:05 +0200 Subject: [PATCH 1/6] allow silencing threads --- bot/exts/moderation/silence.py | 41 ++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index a299d7eedd..8f036661d4 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -178,12 +178,6 @@ async def silence( channel_info = f"#{channel} ({channel.id})" log.debug(f"{ctx.author} is silencing channel {channel_info}.") - # Since threads don't have specific overrides, we cannot silence them individually. - # The parent channel has to be muted or the thread should be archived. - if isinstance(channel, Thread): - await ctx.send(":x: Threads cannot be silenced.") - return - if not await self._set_silence_overwrites(channel, kick=kick): log.info(f"Tried to silence channel {channel_info} but the channel was already silenced.") await self.send_message(MSG_SILENCE_FAIL, ctx.channel, channel, alert_target=False) @@ -241,6 +235,13 @@ async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bo create_public_threads=overwrite.create_public_threads, send_messages_in_threads=overwrite.send_messages_in_threads ) + + elif isinstance(channel, Thread): + if channel.id in self.scheduler: + return False + else: + await channel.edit(locked=True) + return True else: role = self._verified_voice_role @@ -297,6 +298,11 @@ async def _unsilence_wrapper(self, channel: TextOrVoiceChannel, ctx: Context | N if isinstance(channel, VoiceChannel): overwrite = channel.overwrites_for(self._verified_voice_role) has_channel_overwrites = overwrite.speak is False + + elif isinstance(channel, Thread): + await self.send_message(MSG_UNSILENCE_FAIL, msg_channel, channel, alert_target=False) + return + else: overwrite = channel.overwrites_for(self._everyone_role) has_channel_overwrites = overwrite.send_messages is False or overwrite.add_reactions is False @@ -318,8 +324,11 @@ async def _unsilence(self, channel: TextOrVoiceChannel) -> bool: it, cancel the task, and remove it from the notifier. Notify admins if it has a task but not cached overwrites. - Return `True` if channel permissions were changed, `False` otherwise. + Return `True` if channel was unsilenced, `False` otherwise. """ + if isinstance(channel, Thread): + return await self._unsilence_thread(channel) + # Get stored overwrites, and return if channel is unsilenced prev_overwrites = await self.previous_overwrites.get(channel.id) if channel.id not in self.scheduler and prev_overwrites is None: @@ -372,6 +381,24 @@ async def _unsilence(self, channel: TextOrVoiceChannel) -> bool: ) return True + + async def _unsilence_thread(self, channel: Thread) -> bool: + """ + Unsilence a thread. + + This function works the same as `_unsilence`, the only different behaviour regards unsilencing the channel which doesn't require an edit of the overwrites, instead we unlock the thread. + + Return `True` if the thread was unlocked, `False` otherwise. + """ + if channel.id not in self.scheduler: + log.info(f"Tried to unsilence channel #{channel} ({channel.id}) but the channel was not silenced.") + return False + + await channel.edit(locked=False) + log.info(f"Unsilenced channel #{channel} ({channel.id}).") + self.scheduler.cancel(channel.id) + self.notifier.remove_channel(channel) + return True @staticmethod async def _get_afk_channel(guild: Guild) -> VoiceChannel: From 332bbbd148446cd445d212dadeca612839583e2b Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Mon, 15 Jul 2024 01:08:44 +0200 Subject: [PATCH 2/6] lint files --- bot/exts/moderation/silence.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 8f036661d4..ae7c2eb101 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -235,13 +235,12 @@ async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bo create_public_threads=overwrite.create_public_threads, send_messages_in_threads=overwrite.send_messages_in_threads ) - + elif isinstance(channel, Thread): if channel.id in self.scheduler: return False - else: - await channel.edit(locked=True) - return True + await channel.edit(locked=True) + return True else: role = self._verified_voice_role @@ -298,7 +297,7 @@ async def _unsilence_wrapper(self, channel: TextOrVoiceChannel, ctx: Context | N if isinstance(channel, VoiceChannel): overwrite = channel.overwrites_for(self._verified_voice_role) has_channel_overwrites = overwrite.speak is False - + elif isinstance(channel, Thread): await self.send_message(MSG_UNSILENCE_FAIL, msg_channel, channel, alert_target=False) return @@ -381,12 +380,13 @@ async def _unsilence(self, channel: TextOrVoiceChannel) -> bool: ) return True - + async def _unsilence_thread(self, channel: Thread) -> bool: """ Unsilence a thread. - This function works the same as `_unsilence`, the only different behaviour regards unsilencing the channel which doesn't require an edit of the overwrites, instead we unlock the thread. + This function works the same as `_unsilence`, the only different behaviour regards unsilencing the channel + which doesn't require an edit of the overwrites, instead we unlock the thread. Return `True` if the thread was unlocked, `False` otherwise. """ From 352dd56a97bec676fe6f8306ff389720e38122c9 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Thu, 18 Jul 2024 11:39:02 +0200 Subject: [PATCH 3/6] Update bot/exts/moderation/silence.py Co-authored-by: Daniel Gu --- bot/exts/moderation/silence.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index ae7c2eb101..e1afd955ee 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -299,8 +299,7 @@ async def _unsilence_wrapper(self, channel: TextOrVoiceChannel, ctx: Context | N has_channel_overwrites = overwrite.speak is False elif isinstance(channel, Thread): - await self.send_message(MSG_UNSILENCE_FAIL, msg_channel, channel, alert_target=False) - return + has_channel_overwrites = False else: overwrite = channel.overwrites_for(self._everyone_role) From 3eb787a995582efbb3bcee567b22c4c0c837283a Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Sun, 28 Jul 2024 23:57:58 +0200 Subject: [PATCH 4/6] move logic blocks and try to fix typings --- bot/exts/moderation/silence.py | 70 +++++++++++++++------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index ae7c2eb101..b563ff3b1c 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -2,6 +2,7 @@ from collections import OrderedDict from contextlib import suppress from datetime import UTC, datetime, timedelta +from typing import TYPE_CHECKING from async_rediscache import RedisCache from discord import Guild, PermissionOverwrite, TextChannel, Thread, VoiceChannel @@ -16,6 +17,9 @@ from bot.log import get_logger from bot.utils.lock import LockedResourceError, lock, lock_arg +if TYPE_CHECKING: + from discord.abc import MessageableChannel + log = get_logger(__name__) LOCK_NAMESPACE = "silence" @@ -33,6 +37,7 @@ MSG_UNSILENCE_SUCCESS = f"{constants.Emojis.check_mark} unsilenced {{channel}}." TextOrVoiceChannel = TextChannel | VoiceChannel +SilenceableChannel = TextChannel | VoiceChannel | Thread VOICE_CHANNELS = { constants.Channels.code_help_voice_0: constants.Channels.code_help_chat_0, @@ -57,17 +62,17 @@ def __init__(self, alert_channel: TextChannel): time=MISSING, name=None ) - self._silenced_channels = {} + self._silenced_channels: dict[SilenceableChannel, int] = {} self._alert_channel = alert_channel - def add_channel(self, channel: TextOrVoiceChannel) -> None: + def add_channel(self, channel: SilenceableChannel) -> None: """Add channel to `_silenced_channels` and start loop if not launched.""" if not self._silenced_channels: self.start() log.info("Starting notifier loop.") self._silenced_channels[channel] = self._current_loop - def remove_channel(self, channel: TextChannel) -> None: + def remove_channel(self, channel: SilenceableChannel) -> None: """Remove channel from `_silenced_channels` and stop loop if no channels remain.""" with suppress(KeyError): del self._silenced_channels[channel] @@ -92,7 +97,7 @@ async def _notifier(self) -> None: ) -async def _select_lock_channel(args: OrderedDict[str, any]) -> TextOrVoiceChannel: +async def _select_lock_channel(args: OrderedDict[str, any]) -> SilenceableChannel: """Passes the channel to be silenced to the resource lock.""" channel, _ = Silence.parse_silence_args(args["ctx"], args["duration_or_channel"], args["duration"]) return channel @@ -130,8 +135,8 @@ async def cog_load(self) -> None: async def send_message( self, message: str, - source_channel: TextChannel, - target_channel: TextOrVoiceChannel, + source_channel: "MessageableChannel", + target_channel: SilenceableChannel, *, alert_target: bool = False ) -> None: @@ -159,7 +164,7 @@ async def send_message( async def silence( self, ctx: Context, - duration_or_channel: TextOrVoiceChannel | HushDurationConverter = None, + duration_or_channel: SilenceableChannel | HushDurationConverter | None = None, duration: HushDurationConverter = 10, *, kick: bool = False @@ -204,12 +209,12 @@ async def silence( @staticmethod def parse_silence_args( ctx: Context, - duration_or_channel: TextOrVoiceChannel | int, + duration_or_channel: SilenceableChannel | int | None, duration: HushDurationConverter - ) -> tuple[TextOrVoiceChannel, int | None]: + ) -> tuple[SilenceableChannel, int | None]: """Helper method to parse the arguments of the silence command.""" if duration_or_channel: - if isinstance(duration_or_channel, TextChannel | VoiceChannel): + if isinstance(duration_or_channel, TextChannel | VoiceChannel | Thread): channel = duration_or_channel else: channel = ctx.channel @@ -222,8 +227,11 @@ def parse_silence_args( return channel, duration - async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bool = False) -> bool: + async def _set_silence_overwrites(self, channel: SilenceableChannel, *, kick: bool = False) -> bool: """Set silence permission overwrites for `channel` and return True if successful.""" + if channel.id in self.scheduler: + return False + # Get the original channel overwrites if isinstance(channel, TextChannel): role = self._everyone_role @@ -237,8 +245,6 @@ async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bo ) elif isinstance(channel, Thread): - if channel.id in self.scheduler: - return False await channel.edit(locked=True) return True @@ -250,7 +256,7 @@ async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bo prev_overwrites.update(connect=overwrite.connect) # Stop if channel was already silenced - if channel.id in self.scheduler or all(val is False for val in prev_overwrites.values()): + if all(val is False for val in prev_overwrites.values()): return False # Set new permissions, store @@ -260,7 +266,7 @@ async def _set_silence_overwrites(self, channel: TextOrVoiceChannel, *, kick: bo return True - async def _schedule_unsilence(self, ctx: Context, channel: TextOrVoiceChannel, duration: int | None) -> None: + async def _schedule_unsilence(self, ctx: Context, channel: SilenceableChannel, duration: int | None) -> None: """Schedule `ctx.channel` to be unsilenced if `duration` is not None.""" if duration is None: await self.unsilence_timestamps.set(channel.id, -1) @@ -270,7 +276,7 @@ async def _schedule_unsilence(self, ctx: Context, channel: TextOrVoiceChannel, d await self.unsilence_timestamps.set(channel.id, unsilence_time.timestamp()) @commands.command(aliases=("unhush",)) - async def unsilence(self, ctx: Context, *, channel: TextOrVoiceChannel = None) -> None: + async def unsilence(self, ctx: Context, *, channel: SilenceableChannel | None = None) -> None: """ Unsilence the given channel if given, else the current one. @@ -282,7 +288,7 @@ async def unsilence(self, ctx: Context, *, channel: TextOrVoiceChannel = None) - await self._unsilence_wrapper(channel, ctx) @lock_arg(LOCK_NAMESPACE, "channel", raise_error=True) - async def _unsilence_wrapper(self, channel: TextOrVoiceChannel, ctx: Context | None = None) -> None: + async def _unsilence_wrapper(self, channel: SilenceableChannel, ctx: Context | None = None) -> None: """ Unsilence `channel` and send a success/failure message to ctx.channel. @@ -315,7 +321,7 @@ async def _unsilence_wrapper(self, channel: TextOrVoiceChannel, ctx: Context | N else: await self.send_message(MSG_UNSILENCE_SUCCESS, msg_channel, channel, alert_target=True) - async def _unsilence(self, channel: TextOrVoiceChannel) -> bool: + async def _unsilence(self, channel: SilenceableChannel) -> bool: """ Unsilence `channel`. @@ -325,9 +331,6 @@ async def _unsilence(self, channel: TextOrVoiceChannel) -> bool: Return `True` if channel was unsilenced, `False` otherwise. """ - if isinstance(channel, Thread): - return await self._unsilence_thread(channel) - # Get stored overwrites, and return if channel is unsilenced prev_overwrites = await self.previous_overwrites.get(channel.id) if channel.id not in self.scheduler and prev_overwrites is None: @@ -339,6 +342,12 @@ async def _unsilence(self, channel: TextOrVoiceChannel) -> bool: role = self._everyone_role overwrite = channel.overwrites_for(role) permissions = "`Send Messages` and `Add Reactions`" + elif isinstance(channel, Thread): + await channel.edit(locked=False) + log.info(f"Unsilenced channel #{channel} ({channel.id}).") + self.scheduler.cancel(channel.id) + self.notifier.remove_channel(channel) + return True else: role = self._verified_voice_role overwrite = channel.overwrites_for(role) @@ -381,25 +390,6 @@ async def _unsilence(self, channel: TextOrVoiceChannel) -> bool: return True - async def _unsilence_thread(self, channel: Thread) -> bool: - """ - Unsilence a thread. - - This function works the same as `_unsilence`, the only different behaviour regards unsilencing the channel - which doesn't require an edit of the overwrites, instead we unlock the thread. - - Return `True` if the thread was unlocked, `False` otherwise. - """ - if channel.id not in self.scheduler: - log.info(f"Tried to unsilence channel #{channel} ({channel.id}) but the channel was not silenced.") - return False - - await channel.edit(locked=False) - log.info(f"Unsilenced channel #{channel} ({channel.id}).") - self.scheduler.cancel(channel.id) - self.notifier.remove_channel(channel) - return True - @staticmethod async def _get_afk_channel(guild: Guild) -> VoiceChannel: """Get a guild's AFK channel, or create one if it does not exist.""" From b488e8083f5cb59f422e23bb3997752f6c855bc9 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Mon, 29 Jul 2024 09:13:38 +0200 Subject: [PATCH 5/6] Update bot/exts/moderation/silence.py Co-authored-by: Daniel Gu --- bot/exts/moderation/silence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index bd3659af15..f8fa6a0087 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -343,7 +343,7 @@ async def _unsilence(self, channel: SilenceableChannel) -> bool: permissions = "`Send Messages` and `Add Reactions`" elif isinstance(channel, Thread): await channel.edit(locked=False) - log.info(f"Unsilenced channel #{channel} ({channel.id}).") + log.info(f"Unsilenced thread #{channel.parent}/{channel} ({channel.parent_id}/{channel.id}).") self.scheduler.cancel(channel.id) self.notifier.remove_channel(channel) return True From f19fc4ab7e1f391755d7b6179fde4a7d16dc7ef7 Mon Sep 17 00:00:00 2001 From: Snipy7374 <100313469+Snipy7374@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:04:54 +0200 Subject: [PATCH 6/6] fix typing issues and add log message when silencing a thread --- bot/exts/moderation/silence.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index f8fa6a0087..f80b7883aa 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -2,10 +2,11 @@ from collections import OrderedDict from contextlib import suppress from datetime import UTC, datetime, timedelta -from typing import TYPE_CHECKING +from typing import Any from async_rediscache import RedisCache from discord import Guild, PermissionOverwrite, TextChannel, Thread, VoiceChannel +from discord.abc import Messageable from discord.ext import commands, tasks from discord.ext.commands import Context from discord.utils import MISSING @@ -17,9 +18,6 @@ from bot.log import get_logger from bot.utils.lock import LockedResourceError, lock, lock_arg -if TYPE_CHECKING: - from discord.abc import MessageableChannel - log = get_logger(__name__) LOCK_NAMESPACE = "silence" @@ -97,7 +95,7 @@ async def _notifier(self) -> None: ) -async def _select_lock_channel(args: OrderedDict[str, any]) -> SilenceableChannel: +async def _select_lock_channel(args: OrderedDict[str, Any]) -> SilenceableChannel: """Passes the channel to be silenced to the resource lock.""" channel, _ = Silence.parse_silence_args(args["ctx"], args["duration_or_channel"], args["duration"]) return channel @@ -135,7 +133,7 @@ async def cog_load(self) -> None: async def send_message( self, message: str, - source_channel: "MessageableChannel", + source_channel: Messageable, target_channel: SilenceableChannel, *, alert_target: bool = False @@ -245,6 +243,7 @@ async def _set_silence_overwrites(self, channel: SilenceableChannel, *, kick: bo ) elif isinstance(channel, Thread): + log.info(f"Silencing thread #{channel.parent}/{channel} ({channel.parent.id}/{channel.id}).") await channel.edit(locked=True) return True