We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Use scheduler for silencing
I was looking through the code of [`silencer.py`](https://github.com/python-discord/bot/blob/1d3acb0a222a885c5ef42e91570a00f91976fb5b/bot/cogs/moderation/silence.py) and I found this:

https://github.com/python-discord/bot/blob/1d3acb0a222a885c5ef42e91570a00f91976fb5b/bot/cogs/moderation/silence.py#L93

With this approach, in case the channel was unsilenced and re-silenced back, it might get unsilenced by this timer. This is a relatively rare occurrence as this would have to happen within the 15 minutes limit, but nevertheless it seems more logical to use `Scheduler` in here, which can be canceled when the channel is manually unsilenced, to prevent this.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bot/cogs/moderation/silence.py`
Content:
```
1 import asyncio
2 import logging
3 from contextlib import suppress
4 from typing import Optional
5 
6 from discord import TextChannel
7 from discord.ext import commands, tasks
8 from discord.ext.commands import Context
9 
10 from bot.bot import Bot
11 from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles
12 from bot.converters import HushDurationConverter
13 from bot.utils.checks import with_role_check
14 
15 log = logging.getLogger(__name__)
16 
17 
18 class SilenceNotifier(tasks.Loop):
19     """Loop notifier for posting notices to `alert_channel` containing added channels."""
20 
21     def __init__(self, alert_channel: TextChannel):
22         super().__init__(self._notifier, seconds=1, minutes=0, hours=0, count=None, reconnect=True, loop=None)
23         self._silenced_channels = {}
24         self._alert_channel = alert_channel
25 
26     def add_channel(self, channel: TextChannel) -> None:
27         """Add channel to `_silenced_channels` and start loop if not launched."""
28         if not self._silenced_channels:
29             self.start()
30             log.info("Starting notifier loop.")
31         self._silenced_channels[channel] = self._current_loop
32 
33     def remove_channel(self, channel: TextChannel) -> None:
34         """Remove channel from `_silenced_channels` and stop loop if no channels remain."""
35         with suppress(KeyError):
36             del self._silenced_channels[channel]
37             if not self._silenced_channels:
38                 self.stop()
39                 log.info("Stopping notifier loop.")
40 
41     async def _notifier(self) -> None:
42         """Post notice of `_silenced_channels` with their silenced duration to `_alert_channel` periodically."""
43         # Wait for 15 minutes between notices with pause at start of loop.
44         if self._current_loop and not self._current_loop/60 % 15:
45             log.debug(
46                 f"Sending notice with channels: "
47                 f"{', '.join(f'#{channel} ({channel.id})' for channel in self._silenced_channels)}."
48             )
49             channels_text = ', '.join(
50                 f"{channel.mention} for {(self._current_loop-start)//60} min"
51                 for channel, start in self._silenced_channels.items()
52             )
53             await self._alert_channel.send(f"<@&{Roles.moderators}> currently silenced channels: {channels_text}")
54 
55 
56 class Silence(commands.Cog):
57     """Commands for stopping channel messages for `verified` role in a channel."""
58 
59     def __init__(self, bot: Bot):
60         self.bot = bot
61         self.muted_channels = set()
62         self._get_instance_vars_task = self.bot.loop.create_task(self._get_instance_vars())
63         self._get_instance_vars_event = asyncio.Event()
64 
65     async def _get_instance_vars(self) -> None:
66         """Get instance variables after they're available to get from the guild."""
67         await self.bot.wait_until_guild_available()
68         guild = self.bot.get_guild(Guild.id)
69         self._verified_role = guild.get_role(Roles.verified)
70         self._mod_alerts_channel = self.bot.get_channel(Channels.mod_alerts)
71         self._mod_log_channel = self.bot.get_channel(Channels.mod_log)
72         self.notifier = SilenceNotifier(self._mod_log_channel)
73         self._get_instance_vars_event.set()
74 
75     @commands.command(aliases=("hush",))
76     async def silence(self, ctx: Context, duration: HushDurationConverter = 10) -> None:
77         """
78         Silence the current channel for `duration` minutes or `forever`.
79 
80         Duration is capped at 15 minutes, passing forever makes the silence indefinite.
81         Indefinitely silenced channels get added to a notifier which posts notices every 15 minutes from the start.
82         """
83         await self._get_instance_vars_event.wait()
84         log.debug(f"{ctx.author} is silencing channel #{ctx.channel}.")
85         if not await self._silence(ctx.channel, persistent=(duration is None), duration=duration):
86             await ctx.send(f"{Emojis.cross_mark} current channel is already silenced.")
87             return
88         if duration is None:
89             await ctx.send(f"{Emojis.check_mark} silenced current channel indefinitely.")
90             return
91 
92         await ctx.send(f"{Emojis.check_mark} silenced current channel for {duration} minute(s).")
93         await asyncio.sleep(duration*60)
94         log.info("Unsilencing channel after set delay.")
95         await ctx.invoke(self.unsilence)
96 
97     @commands.command(aliases=("unhush",))
98     async def unsilence(self, ctx: Context) -> None:
99         """
100         Unsilence the current channel.
101 
102         If the channel was silenced indefinitely, notifications for the channel will stop.
103         """
104         await self._get_instance_vars_event.wait()
105         log.debug(f"Unsilencing channel #{ctx.channel} from {ctx.author}'s command.")
106         if await self._unsilence(ctx.channel):
107             await ctx.send(f"{Emojis.check_mark} unsilenced current channel.")
108 
109     async def _silence(self, channel: TextChannel, persistent: bool, duration: Optional[int]) -> bool:
110         """
111         Silence `channel` for `self._verified_role`.
112 
113         If `persistent` is `True` add `channel` to notifier.
114         `duration` is only used for logging; if None is passed `persistent` should be True to not log None.
115         Return `True` if channel permissions were changed, `False` otherwise.
116         """
117         current_overwrite = channel.overwrites_for(self._verified_role)
118         if current_overwrite.send_messages is False:
119             log.info(f"Tried to silence channel #{channel} ({channel.id}) but the channel was already silenced.")
120             return False
121         await channel.set_permissions(self._verified_role, **dict(current_overwrite, send_messages=False))
122         self.muted_channels.add(channel)
123         if persistent:
124             log.info(f"Silenced #{channel} ({channel.id}) indefinitely.")
125             self.notifier.add_channel(channel)
126             return True
127 
128         log.info(f"Silenced #{channel} ({channel.id}) for {duration} minute(s).")
129         return True
130 
131     async def _unsilence(self, channel: TextChannel) -> bool:
132         """
133         Unsilence `channel`.
134 
135         Check if `channel` is silenced through a `PermissionOverwrite`,
136         if it is unsilence it and remove it from the notifier.
137         Return `True` if channel permissions were changed, `False` otherwise.
138         """
139         current_overwrite = channel.overwrites_for(self._verified_role)
140         if current_overwrite.send_messages is False:
141             await channel.set_permissions(self._verified_role, **dict(current_overwrite, send_messages=None))
142             log.info(f"Unsilenced channel #{channel} ({channel.id}).")
143             self.notifier.remove_channel(channel)
144             self.muted_channels.discard(channel)
145             return True
146         log.info(f"Tried to unsilence channel #{channel} ({channel.id}) but the channel was not silenced.")
147         return False
148 
149     def cog_unload(self) -> None:
150         """Send alert with silenced channels on unload."""
151         if self.muted_channels:
152             channels_string = ''.join(channel.mention for channel in self.muted_channels)
153             message = f"<@&{Roles.moderators}> channels left silenced on cog unload: {channels_string}"
154             asyncio.create_task(self._mod_alerts_channel.send(message))
155 
156     # This cannot be static (must have a __func__ attribute).
157     def cog_check(self, ctx: Context) -> bool:
158         """Only allow moderators to invoke the commands in this cog."""
159         return with_role_check(ctx, *MODERATION_ROLES)
160
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py
--- a/bot/cogs/moderation/silence.py
+++ b/bot/cogs/moderation/silence.py
@@ -1,7 +1,7 @@
 import asyncio
 import logging
 from contextlib import suppress
-from typing import Optional
+from typing import NamedTuple, Optional
 
 from discord import TextChannel
 from discord.ext import commands, tasks
@@ -11,10 +11,18 @@
 from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles
 from bot.converters import HushDurationConverter
 from bot.utils.checks import with_role_check
+from bot.utils.scheduling import Scheduler
 
 log = logging.getLogger(__name__)
 
 
+class TaskData(NamedTuple):
+    """Data for a scheduled task."""
+
+    delay: int
+    ctx: Context
+
+
 class SilenceNotifier(tasks.Loop):
     """Loop notifier for posting notices to `alert_channel` containing added channels."""
 
@@ -53,15 +61,25 @@
             await self._alert_channel.send(f"<@&{Roles.moderators}> currently silenced channels: {channels_text}")
 
 
-class Silence(commands.Cog):
+class Silence(Scheduler, commands.Cog):
     """Commands for stopping channel messages for `verified` role in a channel."""
 
     def __init__(self, bot: Bot):
+        super().__init__()
         self.bot = bot
         self.muted_channels = set()
         self._get_instance_vars_task = self.bot.loop.create_task(self._get_instance_vars())
         self._get_instance_vars_event = asyncio.Event()
 
+    async def _scheduled_task(self, task: TaskData) -> None:
+        """Calls `self.unsilence` on expired silenced channel to unsilence it."""
+        await asyncio.sleep(task.delay)
+        log.info("Unsilencing channel after set delay.")
+
+        # Because `self.unsilence` explicitly cancels this scheduled task, it is shielded
+        # to avoid prematurely cancelling itself
+        await asyncio.shield(task.ctx.invoke(self.unsilence))
+
     async def _get_instance_vars(self) -> None:
         """Get instance variables after they're available to get from the guild."""
         await self.bot.wait_until_guild_available()
@@ -90,9 +108,13 @@
             return
 
         await ctx.send(f"{Emojis.check_mark} silenced current channel for {duration} minute(s).")
-        await asyncio.sleep(duration*60)
-        log.info("Unsilencing channel after set delay.")
-        await ctx.invoke(self.unsilence)
+
+        task_data = TaskData(
+            delay=duration*60,
+            ctx=ctx
+        )
+
+        self.schedule_task(ctx.channel.id, task_data)
 
     @commands.command(aliases=("unhush",))
     async def unsilence(self, ctx: Context) -> None:
@@ -103,7 +125,9 @@
         """
         await self._get_instance_vars_event.wait()
         log.debug(f"Unsilencing channel #{ctx.channel} from {ctx.author}'s command.")
-        if await self._unsilence(ctx.channel):
+        if not await self._unsilence(ctx.channel):
+            await ctx.send(f"{Emojis.cross_mark} current channel was not silenced.")
+        else:
             await ctx.send(f"{Emojis.check_mark} unsilenced current channel.")
 
     async def _silence(self, channel: TextChannel, persistent: bool, duration: Optional[int]) -> bool:
@@ -140,6 +164,7 @@
         if current_overwrite.send_messages is False:
             await channel.set_permissions(self._verified_role, **dict(current_overwrite, send_messages=None))
             log.info(f"Unsilenced channel #{channel} ({channel.id}).")
+            self.cancel_task(channel.id)
             self.notifier.remove_channel(channel)
             self.muted_channels.discard(channel)
             return True

{"golden_diff": "diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py\n--- a/bot/cogs/moderation/silence.py\n+++ b/bot/cogs/moderation/silence.py\n@@ -1,7 +1,7 @@\n import asyncio\n import logging\n from contextlib import suppress\n-from typing import Optional\n+from typing import NamedTuple, Optional\n \n from discord import TextChannel\n from discord.ext import commands, tasks\n@@ -11,10 +11,18 @@\n from bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles\n from bot.converters import HushDurationConverter\n from bot.utils.checks import with_role_check\n+from bot.utils.scheduling import Scheduler\n \n log = logging.getLogger(__name__)\n \n \n+class TaskData(NamedTuple):\n+    \"\"\"Data for a scheduled task.\"\"\"\n+\n+    delay: int\n+    ctx: Context\n+\n+\n class SilenceNotifier(tasks.Loop):\n     \"\"\"Loop notifier for posting notices to `alert_channel` containing added channels.\"\"\"\n \n@@ -53,15 +61,25 @@\n             await self._alert_channel.send(f\"<@&{Roles.moderators}> currently silenced channels: {channels_text}\")\n \n \n-class Silence(commands.Cog):\n+class Silence(Scheduler, commands.Cog):\n     \"\"\"Commands for stopping channel messages for `verified` role in a channel.\"\"\"\n \n     def __init__(self, bot: Bot):\n+        super().__init__()\n         self.bot = bot\n         self.muted_channels = set()\n         self._get_instance_vars_task = self.bot.loop.create_task(self._get_instance_vars())\n         self._get_instance_vars_event = asyncio.Event()\n \n+    async def _scheduled_task(self, task: TaskData) -> None:\n+        \"\"\"Calls `self.unsilence` on expired silenced channel to unsilence it.\"\"\"\n+        await asyncio.sleep(task.delay)\n+        log.info(\"Unsilencing channel after set delay.\")\n+\n+        # Because `self.unsilence` explicitly cancels this scheduled task, it is shielded\n+        # to avoid prematurely cancelling itself\n+        await asyncio.shield(task.ctx.invoke(self.unsilence))\n+\n     async def _get_instance_vars(self) -> None:\n         \"\"\"Get instance variables after they're available to get from the guild.\"\"\"\n         await self.bot.wait_until_guild_available()\n@@ -90,9 +108,13 @@\n             return\n \n         await ctx.send(f\"{Emojis.check_mark} silenced current channel for {duration} minute(s).\")\n-        await asyncio.sleep(duration*60)\n-        log.info(\"Unsilencing channel after set delay.\")\n-        await ctx.invoke(self.unsilence)\n+\n+        task_data = TaskData(\n+            delay=duration*60,\n+            ctx=ctx\n+        )\n+\n+        self.schedule_task(ctx.channel.id, task_data)\n \n     @commands.command(aliases=(\"unhush\",))\n     async def unsilence(self, ctx: Context) -> None:\n@@ -103,7 +125,9 @@\n         \"\"\"\n         await self._get_instance_vars_event.wait()\n         log.debug(f\"Unsilencing channel #{ctx.channel} from {ctx.author}'s command.\")\n-        if await self._unsilence(ctx.channel):\n+        if not await self._unsilence(ctx.channel):\n+            await ctx.send(f\"{Emojis.cross_mark} current channel was not silenced.\")\n+        else:\n             await ctx.send(f\"{Emojis.check_mark} unsilenced current channel.\")\n \n     async def _silence(self, channel: TextChannel, persistent: bool, duration: Optional[int]) -> bool:\n@@ -140,6 +164,7 @@\n         if current_overwrite.send_messages is False:\n             await channel.set_permissions(self._verified_role, **dict(current_overwrite, send_messages=None))\n             log.info(f\"Unsilenced channel #{channel} ({channel.id}).\")\n+            self.cancel_task(channel.id)\n             self.notifier.remove_channel(channel)\n             self.muted_channels.discard(channel)\n             return True\n", "issue": "Use scheduler for silencing\nI was looking through the code of [`silencer.py`](https://github.com/python-discord/bot/blob/1d3acb0a222a885c5ef42e91570a00f91976fb5b/bot/cogs/moderation/silence.py) and I found this:\r\n\r\nhttps://github.com/python-discord/bot/blob/1d3acb0a222a885c5ef42e91570a00f91976fb5b/bot/cogs/moderation/silence.py#L93\r\n\r\nWith this approach, in case the channel was unsilenced and re-silenced back, it might get unsilenced by this timer. This is a relatively rare occurrence as this would have to happen within the 15 minutes limit, but nevertheless it seems more logical to use `Scheduler` in here, which can be canceled when the channel is manually unsilenced, to prevent this.\n", "before_files": [{"content": "import asyncio\nimport logging\nfrom contextlib import suppress\nfrom typing import Optional\n\nfrom discord import TextChannel\nfrom discord.ext import commands, tasks\nfrom discord.ext.commands import Context\n\nfrom bot.bot import Bot\nfrom bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles\nfrom bot.converters import HushDurationConverter\nfrom bot.utils.checks import with_role_check\n\nlog = logging.getLogger(__name__)\n\n\nclass SilenceNotifier(tasks.Loop):\n    \"\"\"Loop notifier for posting notices to `alert_channel` containing added channels.\"\"\"\n\n    def __init__(self, alert_channel: TextChannel):\n        super().__init__(self._notifier, seconds=1, minutes=0, hours=0, count=None, reconnect=True, loop=None)\n        self._silenced_channels = {}\n        self._alert_channel = alert_channel\n\n    def add_channel(self, channel: TextChannel) -> None:\n        \"\"\"Add channel to `_silenced_channels` and start loop if not launched.\"\"\"\n        if not self._silenced_channels:\n            self.start()\n            log.info(\"Starting notifier loop.\")\n        self._silenced_channels[channel] = self._current_loop\n\n    def remove_channel(self, channel: TextChannel) -> None:\n        \"\"\"Remove channel from `_silenced_channels` and stop loop if no channels remain.\"\"\"\n        with suppress(KeyError):\n            del self._silenced_channels[channel]\n            if not self._silenced_channels:\n                self.stop()\n                log.info(\"Stopping notifier loop.\")\n\n    async def _notifier(self) -> None:\n        \"\"\"Post notice of `_silenced_channels` with their silenced duration to `_alert_channel` periodically.\"\"\"\n        # Wait for 15 minutes between notices with pause at start of loop.\n        if self._current_loop and not self._current_loop/60 % 15:\n            log.debug(\n                f\"Sending notice with channels: \"\n                f\"{', '.join(f'#{channel} ({channel.id})' for channel in self._silenced_channels)}.\"\n            )\n            channels_text = ', '.join(\n                f\"{channel.mention} for {(self._current_loop-start)//60} min\"\n                for channel, start in self._silenced_channels.items()\n            )\n            await self._alert_channel.send(f\"<@&{Roles.moderators}> currently silenced channels: {channels_text}\")\n\n\nclass Silence(commands.Cog):\n    \"\"\"Commands for stopping channel messages for `verified` role in a channel.\"\"\"\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.muted_channels = set()\n        self._get_instance_vars_task = self.bot.loop.create_task(self._get_instance_vars())\n        self._get_instance_vars_event = asyncio.Event()\n\n    async def _get_instance_vars(self) -> None:\n        \"\"\"Get instance variables after they're available to get from the guild.\"\"\"\n        await self.bot.wait_until_guild_available()\n        guild = self.bot.get_guild(Guild.id)\n        self._verified_role = guild.get_role(Roles.verified)\n        self._mod_alerts_channel = self.bot.get_channel(Channels.mod_alerts)\n        self._mod_log_channel = self.bot.get_channel(Channels.mod_log)\n        self.notifier = SilenceNotifier(self._mod_log_channel)\n        self._get_instance_vars_event.set()\n\n    @commands.command(aliases=(\"hush\",))\n    async def silence(self, ctx: Context, duration: HushDurationConverter = 10) -> None:\n        \"\"\"\n        Silence the current channel for `duration` minutes or `forever`.\n\n        Duration is capped at 15 minutes, passing forever makes the silence indefinite.\n        Indefinitely silenced channels get added to a notifier which posts notices every 15 minutes from the start.\n        \"\"\"\n        await self._get_instance_vars_event.wait()\n        log.debug(f\"{ctx.author} is silencing channel #{ctx.channel}.\")\n        if not await self._silence(ctx.channel, persistent=(duration is None), duration=duration):\n            await ctx.send(f\"{Emojis.cross_mark} current channel is already silenced.\")\n            return\n        if duration is None:\n            await ctx.send(f\"{Emojis.check_mark} silenced current channel indefinitely.\")\n            return\n\n        await ctx.send(f\"{Emojis.check_mark} silenced current channel for {duration} minute(s).\")\n        await asyncio.sleep(duration*60)\n        log.info(\"Unsilencing channel after set delay.\")\n        await ctx.invoke(self.unsilence)\n\n    @commands.command(aliases=(\"unhush\",))\n    async def unsilence(self, ctx: Context) -> None:\n        \"\"\"\n        Unsilence the current channel.\n\n        If the channel was silenced indefinitely, notifications for the channel will stop.\n        \"\"\"\n        await self._get_instance_vars_event.wait()\n        log.debug(f\"Unsilencing channel #{ctx.channel} from {ctx.author}'s command.\")\n        if await self._unsilence(ctx.channel):\n            await ctx.send(f\"{Emojis.check_mark} unsilenced current channel.\")\n\n    async def _silence(self, channel: TextChannel, persistent: bool, duration: Optional[int]) -> bool:\n        \"\"\"\n        Silence `channel` for `self._verified_role`.\n\n        If `persistent` is `True` add `channel` to notifier.\n        `duration` is only used for logging; if None is passed `persistent` should be True to not log None.\n        Return `True` if channel permissions were changed, `False` otherwise.\n        \"\"\"\n        current_overwrite = channel.overwrites_for(self._verified_role)\n        if current_overwrite.send_messages is False:\n            log.info(f\"Tried to silence channel #{channel} ({channel.id}) but the channel was already silenced.\")\n            return False\n        await channel.set_permissions(self._verified_role, **dict(current_overwrite, send_messages=False))\n        self.muted_channels.add(channel)\n        if persistent:\n            log.info(f\"Silenced #{channel} ({channel.id}) indefinitely.\")\n            self.notifier.add_channel(channel)\n            return True\n\n        log.info(f\"Silenced #{channel} ({channel.id}) for {duration} minute(s).\")\n        return True\n\n    async def _unsilence(self, channel: TextChannel) -> bool:\n        \"\"\"\n        Unsilence `channel`.\n\n        Check if `channel` is silenced through a `PermissionOverwrite`,\n        if it is unsilence it and remove it from the notifier.\n        Return `True` if channel permissions were changed, `False` otherwise.\n        \"\"\"\n        current_overwrite = channel.overwrites_for(self._verified_role)\n        if current_overwrite.send_messages is False:\n            await channel.set_permissions(self._verified_role, **dict(current_overwrite, send_messages=None))\n            log.info(f\"Unsilenced channel #{channel} ({channel.id}).\")\n            self.notifier.remove_channel(channel)\n            self.muted_channels.discard(channel)\n            return True\n        log.info(f\"Tried to unsilence channel #{channel} ({channel.id}) but the channel was not silenced.\")\n        return False\n\n    def cog_unload(self) -> None:\n        \"\"\"Send alert with silenced channels on unload.\"\"\"\n        if self.muted_channels:\n            channels_string = ''.join(channel.mention for channel in self.muted_channels)\n            message = f\"<@&{Roles.moderators}> channels left silenced on cog unload: {channels_string}\"\n            asyncio.create_task(self._mod_alerts_channel.send(message))\n\n    # This cannot be static (must have a __func__ attribute).\n    def cog_check(self, ctx: Context) -> bool:\n        \"\"\"Only allow moderators to invoke the commands in this cog.\"\"\"\n        return with_role_check(ctx, *MODERATION_ROLES)\n", "path": "bot/cogs/moderation/silence.py"}], "after_files": [{"content": "import asyncio\nimport logging\nfrom contextlib import suppress\nfrom typing import NamedTuple, Optional\n\nfrom discord import TextChannel\nfrom discord.ext import commands, tasks\nfrom discord.ext.commands import Context\n\nfrom bot.bot import Bot\nfrom bot.constants import Channels, Emojis, Guild, MODERATION_ROLES, Roles\nfrom bot.converters import HushDurationConverter\nfrom bot.utils.checks import with_role_check\nfrom bot.utils.scheduling import Scheduler\n\nlog = logging.getLogger(__name__)\n\n\nclass TaskData(NamedTuple):\n    \"\"\"Data for a scheduled task.\"\"\"\n\n    delay: int\n    ctx: Context\n\n\nclass SilenceNotifier(tasks.Loop):\n    \"\"\"Loop notifier for posting notices to `alert_channel` containing added channels.\"\"\"\n\n    def __init__(self, alert_channel: TextChannel):\n        super().__init__(self._notifier, seconds=1, minutes=0, hours=0, count=None, reconnect=True, loop=None)\n        self._silenced_channels = {}\n        self._alert_channel = alert_channel\n\n    def add_channel(self, channel: TextChannel) -> None:\n        \"\"\"Add channel to `_silenced_channels` and start loop if not launched.\"\"\"\n        if not self._silenced_channels:\n            self.start()\n            log.info(\"Starting notifier loop.\")\n        self._silenced_channels[channel] = self._current_loop\n\n    def remove_channel(self, channel: TextChannel) -> None:\n        \"\"\"Remove channel from `_silenced_channels` and stop loop if no channels remain.\"\"\"\n        with suppress(KeyError):\n            del self._silenced_channels[channel]\n            if not self._silenced_channels:\n                self.stop()\n                log.info(\"Stopping notifier loop.\")\n\n    async def _notifier(self) -> None:\n        \"\"\"Post notice of `_silenced_channels` with their silenced duration to `_alert_channel` periodically.\"\"\"\n        # Wait for 15 minutes between notices with pause at start of loop.\n        if self._current_loop and not self._current_loop/60 % 15:\n            log.debug(\n                f\"Sending notice with channels: \"\n                f\"{', '.join(f'#{channel} ({channel.id})' for channel in self._silenced_channels)}.\"\n            )\n            channels_text = ', '.join(\n                f\"{channel.mention} for {(self._current_loop-start)//60} min\"\n                for channel, start in self._silenced_channels.items()\n            )\n            await self._alert_channel.send(f\"<@&{Roles.moderators}> currently silenced channels: {channels_text}\")\n\n\nclass Silence(Scheduler, commands.Cog):\n    \"\"\"Commands for stopping channel messages for `verified` role in a channel.\"\"\"\n\n    def __init__(self, bot: Bot):\n        super().__init__()\n        self.bot = bot\n        self.muted_channels = set()\n        self._get_instance_vars_task = self.bot.loop.create_task(self._get_instance_vars())\n        self._get_instance_vars_event = asyncio.Event()\n\n    async def _scheduled_task(self, task: TaskData) -> None:\n        \"\"\"Calls `self.unsilence` on expired silenced channel to unsilence it.\"\"\"\n        await asyncio.sleep(task.delay)\n        log.info(\"Unsilencing channel after set delay.\")\n\n        # Because `self.unsilence` explicitly cancels this scheduled task, it is shielded\n        # to avoid prematurely cancelling itself\n        await asyncio.shield(task.ctx.invoke(self.unsilence))\n\n    async def _get_instance_vars(self) -> None:\n        \"\"\"Get instance variables after they're available to get from the guild.\"\"\"\n        await self.bot.wait_until_guild_available()\n        guild = self.bot.get_guild(Guild.id)\n        self._verified_role = guild.get_role(Roles.verified)\n        self._mod_alerts_channel = self.bot.get_channel(Channels.mod_alerts)\n        self._mod_log_channel = self.bot.get_channel(Channels.mod_log)\n        self.notifier = SilenceNotifier(self._mod_log_channel)\n        self._get_instance_vars_event.set()\n\n    @commands.command(aliases=(\"hush\",))\n    async def silence(self, ctx: Context, duration: HushDurationConverter = 10) -> None:\n        \"\"\"\n        Silence the current channel for `duration` minutes or `forever`.\n\n        Duration is capped at 15 minutes, passing forever makes the silence indefinite.\n        Indefinitely silenced channels get added to a notifier which posts notices every 15 minutes from the start.\n        \"\"\"\n        await self._get_instance_vars_event.wait()\n        log.debug(f\"{ctx.author} is silencing channel #{ctx.channel}.\")\n        if not await self._silence(ctx.channel, persistent=(duration is None), duration=duration):\n            await ctx.send(f\"{Emojis.cross_mark} current channel is already silenced.\")\n            return\n        if duration is None:\n            await ctx.send(f\"{Emojis.check_mark} silenced current channel indefinitely.\")\n            return\n\n        await ctx.send(f\"{Emojis.check_mark} silenced current channel for {duration} minute(s).\")\n\n        task_data = TaskData(\n            delay=duration*60,\n            ctx=ctx\n        )\n\n        self.schedule_task(ctx.channel.id, task_data)\n\n    @commands.command(aliases=(\"unhush\",))\n    async def unsilence(self, ctx: Context) -> None:\n        \"\"\"\n        Unsilence the current channel.\n\n        If the channel was silenced indefinitely, notifications for the channel will stop.\n        \"\"\"\n        await self._get_instance_vars_event.wait()\n        log.debug(f\"Unsilencing channel #{ctx.channel} from {ctx.author}'s command.\")\n        if not await self._unsilence(ctx.channel):\n            await ctx.send(f\"{Emojis.cross_mark} current channel was not silenced.\")\n        else:\n            await ctx.send(f\"{Emojis.check_mark} unsilenced current channel.\")\n\n    async def _silence(self, channel: TextChannel, persistent: bool, duration: Optional[int]) -> bool:\n        \"\"\"\n        Silence `channel` for `self._verified_role`.\n\n        If `persistent` is `True` add `channel` to notifier.\n        `duration` is only used for logging; if None is passed `persistent` should be True to not log None.\n        Return `True` if channel permissions were changed, `False` otherwise.\n        \"\"\"\n        current_overwrite = channel.overwrites_for(self._verified_role)\n        if current_overwrite.send_messages is False:\n            log.info(f\"Tried to silence channel #{channel} ({channel.id}) but the channel was already silenced.\")\n            return False\n        await channel.set_permissions(self._verified_role, **dict(current_overwrite, send_messages=False))\n        self.muted_channels.add(channel)\n        if persistent:\n            log.info(f\"Silenced #{channel} ({channel.id}) indefinitely.\")\n            self.notifier.add_channel(channel)\n            return True\n\n        log.info(f\"Silenced #{channel} ({channel.id}) for {duration} minute(s).\")\n        return True\n\n    async def _unsilence(self, channel: TextChannel) -> bool:\n        \"\"\"\n        Unsilence `channel`.\n\n        Check if `channel` is silenced through a `PermissionOverwrite`,\n        if it is unsilence it and remove it from the notifier.\n        Return `True` if channel permissions were changed, `False` otherwise.\n        \"\"\"\n        current_overwrite = channel.overwrites_for(self._verified_role)\n        if current_overwrite.send_messages is False:\n            await channel.set_permissions(self._verified_role, **dict(current_overwrite, send_messages=None))\n            log.info(f\"Unsilenced channel #{channel} ({channel.id}).\")\n            self.cancel_task(channel.id)\n            self.notifier.remove_channel(channel)\n            self.muted_channels.discard(channel)\n            return True\n        log.info(f\"Tried to unsilence channel #{channel} ({channel.id}) but the channel was not silenced.\")\n        return False\n\n    def cog_unload(self) -> None:\n        \"\"\"Send alert with silenced channels on unload.\"\"\"\n        if self.muted_channels:\n            channels_string = ''.join(channel.mention for channel in self.muted_channels)\n            message = f\"<@&{Roles.moderators}> channels left silenced on cog unload: {channels_string}\"\n            asyncio.create_task(self._mod_alerts_channel.send(message))\n\n    # This cannot be static (must have a __func__ attribute).\n    def cog_check(self, ctx: Context) -> bool:\n        \"\"\"Only allow moderators to invoke the commands in this cog.\"\"\"\n        return with_role_check(ctx, *MODERATION_ROLES)\n", "path": "bot/cogs/moderation/silence.py"}]}