We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Stop an in-progress stream if a user's role is revoked
If a user's streaming permission is removed via the revoke stream command, we want to stop any stream that may be in-progress.

If a user's video role drops off due to the normal schedule, then we don't need to do this.

`discord.ActivityType.streaming` shows whether a user is streaming.

This implementation should work even if the member don't have the video role anymore.

Concept approved by @MrHemlock 

--- 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/exts/moderation/stream.py`
Content:
```
1 import logging
2 from datetime import timedelta, timezone
3 from operator import itemgetter
4 
5 import arrow
6 import discord
7 from arrow import Arrow
8 from async_rediscache import RedisCache
9 from discord.ext import commands
10 
11 from bot.bot import Bot
12 from bot.constants import Colours, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES, VideoPermission
13 from bot.converters import Expiry
14 from bot.pagination import LinePaginator
15 from bot.utils.scheduling import Scheduler
16 from bot.utils.time import format_infraction_with_duration
17 
18 log = logging.getLogger(__name__)
19 
20 
21 class Stream(commands.Cog):
22     """Grant and revoke streaming permissions from members."""
23 
24     # Stores tasks to remove streaming permission
25     # RedisCache[discord.Member.id, UtcPosixTimestamp]
26     task_cache = RedisCache()
27 
28     def __init__(self, bot: Bot):
29         self.bot = bot
30         self.scheduler = Scheduler(self.__class__.__name__)
31         self.reload_task = self.bot.loop.create_task(self._reload_tasks_from_redis())
32 
33     def cog_unload(self) -> None:
34         """Cancel all scheduled tasks."""
35         self.reload_task.cancel()
36         self.reload_task.add_done_callback(lambda _: self.scheduler.cancel_all())
37 
38     async def _revoke_streaming_permission(self, member: discord.Member) -> None:
39         """Remove the streaming permission from the given Member."""
40         await self.task_cache.delete(member.id)
41         await member.remove_roles(discord.Object(Roles.video), reason="Streaming access revoked")
42 
43     async def _reload_tasks_from_redis(self) -> None:
44         """Reload outstanding tasks from redis on startup, delete the task if the member has since left the server."""
45         await self.bot.wait_until_guild_available()
46         items = await self.task_cache.items()
47         for key, value in items:
48             member = self.bot.get_guild(Guild.id).get_member(key)
49 
50             if not member:
51                 # Member isn't found in the cache
52                 try:
53                     member = await self.bot.get_guild(Guild.id).fetch_member(key)
54                 except discord.errors.NotFound:
55                     log.debug(
56                         f"Member {key} left the guild before we could schedule "
57                         "the revoking of their streaming permissions."
58                     )
59                     await self.task_cache.delete(key)
60                     continue
61                 except discord.HTTPException:
62                     log.exception(f"Exception while trying to retrieve member {key} from Discord.")
63                     continue
64 
65             revoke_time = Arrow.utcfromtimestamp(value)
66             log.debug(f"Scheduling {member} ({member.id}) to have streaming permission revoked at {revoke_time}")
67             self.scheduler.schedule_at(
68                 revoke_time,
69                 key,
70                 self._revoke_streaming_permission(member)
71             )
72 
73     @commands.command(aliases=("streaming",))
74     @commands.has_any_role(*MODERATION_ROLES)
75     async def stream(self, ctx: commands.Context, member: discord.Member, duration: Expiry = None) -> None:
76         """
77         Temporarily grant streaming permissions to a member for a given duration.
78 
79         A unit of time should be appended to the duration.
80         Units (∗case-sensitive):
81         \u2003`y` - years
82         \u2003`m` - months∗
83         \u2003`w` - weeks
84         \u2003`d` - days
85         \u2003`h` - hours
86         \u2003`M` - minutes∗
87         \u2003`s` - seconds
88 
89         Alternatively, an ISO 8601 timestamp can be provided for the duration.
90         """
91         log.trace(f"Attempting to give temporary streaming permission to {member} ({member.id}).")
92 
93         if duration is None:
94             # Use default duration and convert back to datetime as Embed.timestamp doesn't support Arrow
95             duration = arrow.utcnow() + timedelta(minutes=VideoPermission.default_permission_duration)
96             duration = duration.datetime
97         elif duration.tzinfo is None:
98             # Make duration tz-aware.
99             # ISODateTime could already include tzinfo, this check is so it isn't overwritten.
100             duration.replace(tzinfo=timezone.utc)
101 
102         # Check if the member already has streaming permission
103         already_allowed = any(Roles.video == role.id for role in member.roles)
104         if already_allowed:
105             await ctx.send(f"{Emojis.cross_mark} {member.mention} can already stream.")
106             log.debug(f"{member} ({member.id}) already has permission to stream.")
107             return
108 
109         # Schedule task to remove streaming permission from Member and add it to task cache
110         self.scheduler.schedule_at(duration, member.id, self._revoke_streaming_permission(member))
111         await self.task_cache.set(member.id, duration.timestamp())
112 
113         await member.add_roles(discord.Object(Roles.video), reason="Temporary streaming access granted")
114 
115         # Use embed as embed timestamps do timezone conversions.
116         embed = discord.Embed(
117             description=f"{Emojis.check_mark} {member.mention} can now stream.",
118             colour=Colours.soft_green
119         )
120         embed.set_footer(text=f"Streaming permission has been given to {member} until")
121         embed.timestamp = duration
122 
123         # Mention in content as mentions in embeds don't ping
124         await ctx.send(content=member.mention, embed=embed)
125 
126         # Convert here for nicer logging
127         revoke_time = format_infraction_with_duration(str(duration))
128         log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.")
129 
130     @commands.command(aliases=("pstream",))
131     @commands.has_any_role(*MODERATION_ROLES)
132     async def permanentstream(self, ctx: commands.Context, member: discord.Member) -> None:
133         """Permanently grants the given member the permission to stream."""
134         log.trace(f"Attempting to give permanent streaming permission to {member} ({member.id}).")
135 
136         # Check if the member already has streaming permission
137         if any(Roles.video == role.id for role in member.roles):
138             if member.id in self.scheduler:
139                 # Member has temp permission, so cancel the task to revoke later and delete from cache
140                 self.scheduler.cancel(member.id)
141                 await self.task_cache.delete(member.id)
142 
143                 await ctx.send(f"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.")
144                 log.debug(
145                     f"Successfully upgraded temporary streaming permission for {member} ({member.id}) to permanent."
146                 )
147                 return
148 
149             await ctx.send(f"{Emojis.cross_mark} This member can already stream.")
150             log.debug(f"{member} ({member.id}) already had permanent streaming permission.")
151             return
152 
153         await member.add_roles(discord.Object(Roles.video), reason="Permanent streaming access granted")
154         await ctx.send(f"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.")
155         log.debug(f"Successfully gave {member} ({member.id}) permanent streaming permission.")
156 
157     @commands.command(aliases=("unstream", "rstream"))
158     @commands.has_any_role(*MODERATION_ROLES)
159     async def revokestream(self, ctx: commands.Context, member: discord.Member) -> None:
160         """Revoke the permission to stream from the given member."""
161         log.trace(f"Attempting to remove streaming permission from {member} ({member.id}).")
162 
163         # Check if the member already has streaming permission
164         if any(Roles.video == role.id for role in member.roles):
165             if member.id in self.scheduler:
166                 # Member has temp permission, so cancel the task to revoke later and delete from cache
167                 self.scheduler.cancel(member.id)
168                 await self.task_cache.delete(member.id)
169             await self._revoke_streaming_permission(member)
170 
171             await ctx.send(f"{Emojis.check_mark} Revoked the permission to stream from {member.mention}.")
172             log.debug(f"Successfully revoked streaming permission from {member} ({member.id}).")
173             return
174 
175         await ctx.send(f"{Emojis.cross_mark} This member doesn't have video permissions to remove!")
176         log.debug(f"{member} ({member.id}) didn't have the streaming permission to remove!")
177 
178     @commands.command(aliases=('lstream',))
179     @commands.has_any_role(*MODERATION_ROLES)
180     async def liststream(self, ctx: commands.Context) -> None:
181         """Lists all non-staff users who have permission to stream."""
182         non_staff_members_with_stream = [
183             member
184             for member in ctx.guild.get_role(Roles.video).members
185             if not any(role.id in STAFF_ROLES for role in member.roles)
186         ]
187 
188         # List of tuples (UtcPosixTimestamp, str)
189         # So that the list can be sorted on the UtcPosixTimestamp before the message is passed to the paginator.
190         streamer_info = []
191         for member in non_staff_members_with_stream:
192             if revoke_time := await self.task_cache.get(member.id):
193                 # Member only has temporary streaming perms
194                 revoke_delta = Arrow.utcfromtimestamp(revoke_time).humanize()
195                 message = f"{member.mention} will have stream permissions revoked {revoke_delta}."
196             else:
197                 message = f"{member.mention} has permanent streaming permissions."
198 
199             # If revoke_time is None use max timestamp to force sort to put them at the end
200             streamer_info.append(
201                 (revoke_time or Arrow.max.timestamp(), message)
202             )
203 
204         if streamer_info:
205             # Sort based on duration left of streaming perms
206             streamer_info.sort(key=itemgetter(0))
207 
208             # Only output the message in the pagination
209             lines = [line[1] for line in streamer_info]
210             embed = discord.Embed(
211                 title=f"Members with streaming permission (`{len(lines)}` total)",
212                 colour=Colours.soft_green
213             )
214             await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)
215         else:
216             await ctx.send("No members with stream permissions found.")
217 
218 
219 def setup(bot: Bot) -> None:
220     """Loads the Stream cog."""
221     bot.add_cog(Stream(bot))
222
```
--- 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/exts/moderation/stream.py b/bot/exts/moderation/stream.py
--- a/bot/exts/moderation/stream.py
+++ b/bot/exts/moderation/stream.py
@@ -70,6 +70,28 @@
                 self._revoke_streaming_permission(member)
             )
 
+    async def _suspend_stream(self, ctx: commands.Context, member: discord.Member) -> None:
+        """Suspend a member's stream."""
+        await self.bot.wait_until_guild_available()
+        voice_state = member.voice
+
+        if not voice_state:
+            return
+
+        # If the user is streaming.
+        if voice_state.self_stream:
+            # End user's stream by moving them to AFK voice channel and back.
+            original_vc = voice_state.channel
+            await member.move_to(ctx.guild.afk_channel)
+            await member.move_to(original_vc)
+
+            # Notify.
+            await ctx.send(f"{member.mention}'s stream has been suspended!")
+            log.debug(f"Successfully suspended stream from {member} ({member.id}).")
+            return
+
+        log.debug(f"No stream found to suspend from {member} ({member.id}).")
+
     @commands.command(aliases=("streaming",))
     @commands.has_any_role(*MODERATION_ROLES)
     async def stream(self, ctx: commands.Context, member: discord.Member, duration: Expiry = None) -> None:
@@ -170,10 +192,12 @@
 
             await ctx.send(f"{Emojis.check_mark} Revoked the permission to stream from {member.mention}.")
             log.debug(f"Successfully revoked streaming permission from {member} ({member.id}).")
-            return
 
-        await ctx.send(f"{Emojis.cross_mark} This member doesn't have video permissions to remove!")
-        log.debug(f"{member} ({member.id}) didn't have the streaming permission to remove!")
+        else:
+            await ctx.send(f"{Emojis.cross_mark} This member doesn't have video permissions to remove!")
+            log.debug(f"{member} ({member.id}) didn't have the streaming permission to remove!")
+
+        await self._suspend_stream(ctx, member)
 
     @commands.command(aliases=('lstream',))
     @commands.has_any_role(*MODERATION_ROLES)

{"golden_diff": "diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py\n--- a/bot/exts/moderation/stream.py\n+++ b/bot/exts/moderation/stream.py\n@@ -70,6 +70,28 @@\n                 self._revoke_streaming_permission(member)\n             )\n \n+    async def _suspend_stream(self, ctx: commands.Context, member: discord.Member) -> None:\n+        \"\"\"Suspend a member's stream.\"\"\"\n+        await self.bot.wait_until_guild_available()\n+        voice_state = member.voice\n+\n+        if not voice_state:\n+            return\n+\n+        # If the user is streaming.\n+        if voice_state.self_stream:\n+            # End user's stream by moving them to AFK voice channel and back.\n+            original_vc = voice_state.channel\n+            await member.move_to(ctx.guild.afk_channel)\n+            await member.move_to(original_vc)\n+\n+            # Notify.\n+            await ctx.send(f\"{member.mention}'s stream has been suspended!\")\n+            log.debug(f\"Successfully suspended stream from {member} ({member.id}).\")\n+            return\n+\n+        log.debug(f\"No stream found to suspend from {member} ({member.id}).\")\n+\n     @commands.command(aliases=(\"streaming\",))\n     @commands.has_any_role(*MODERATION_ROLES)\n     async def stream(self, ctx: commands.Context, member: discord.Member, duration: Expiry = None) -> None:\n@@ -170,10 +192,12 @@\n \n             await ctx.send(f\"{Emojis.check_mark} Revoked the permission to stream from {member.mention}.\")\n             log.debug(f\"Successfully revoked streaming permission from {member} ({member.id}).\")\n-            return\n \n-        await ctx.send(f\"{Emojis.cross_mark} This member doesn't have video permissions to remove!\")\n-        log.debug(f\"{member} ({member.id}) didn't have the streaming permission to remove!\")\n+        else:\n+            await ctx.send(f\"{Emojis.cross_mark} This member doesn't have video permissions to remove!\")\n+            log.debug(f\"{member} ({member.id}) didn't have the streaming permission to remove!\")\n+\n+        await self._suspend_stream(ctx, member)\n \n     @commands.command(aliases=('lstream',))\n     @commands.has_any_role(*MODERATION_ROLES)\n", "issue": "Stop an in-progress stream if a user's role is revoked\nIf a user's streaming permission is removed via the revoke stream command, we want to stop any stream that may be in-progress.\r\n\r\nIf a user's video role drops off due to the normal schedule, then we don't need to do this.\r\n\r\n`discord.ActivityType.streaming` shows whether a user is streaming.\r\n\r\nThis implementation should work even if the member don't have the video role anymore.\r\n\r\nConcept approved by @MrHemlock \n", "before_files": [{"content": "import logging\nfrom datetime import timedelta, timezone\nfrom operator import itemgetter\n\nimport arrow\nimport discord\nfrom arrow import Arrow\nfrom async_rediscache import RedisCache\nfrom discord.ext import commands\n\nfrom bot.bot import Bot\nfrom bot.constants import Colours, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES, VideoPermission\nfrom bot.converters import Expiry\nfrom bot.pagination import LinePaginator\nfrom bot.utils.scheduling import Scheduler\nfrom bot.utils.time import format_infraction_with_duration\n\nlog = logging.getLogger(__name__)\n\n\nclass Stream(commands.Cog):\n    \"\"\"Grant and revoke streaming permissions from members.\"\"\"\n\n    # Stores tasks to remove streaming permission\n    # RedisCache[discord.Member.id, UtcPosixTimestamp]\n    task_cache = RedisCache()\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.scheduler = Scheduler(self.__class__.__name__)\n        self.reload_task = self.bot.loop.create_task(self._reload_tasks_from_redis())\n\n    def cog_unload(self) -> None:\n        \"\"\"Cancel all scheduled tasks.\"\"\"\n        self.reload_task.cancel()\n        self.reload_task.add_done_callback(lambda _: self.scheduler.cancel_all())\n\n    async def _revoke_streaming_permission(self, member: discord.Member) -> None:\n        \"\"\"Remove the streaming permission from the given Member.\"\"\"\n        await self.task_cache.delete(member.id)\n        await member.remove_roles(discord.Object(Roles.video), reason=\"Streaming access revoked\")\n\n    async def _reload_tasks_from_redis(self) -> None:\n        \"\"\"Reload outstanding tasks from redis on startup, delete the task if the member has since left the server.\"\"\"\n        await self.bot.wait_until_guild_available()\n        items = await self.task_cache.items()\n        for key, value in items:\n            member = self.bot.get_guild(Guild.id).get_member(key)\n\n            if not member:\n                # Member isn't found in the cache\n                try:\n                    member = await self.bot.get_guild(Guild.id).fetch_member(key)\n                except discord.errors.NotFound:\n                    log.debug(\n                        f\"Member {key} left the guild before we could schedule \"\n                        \"the revoking of their streaming permissions.\"\n                    )\n                    await self.task_cache.delete(key)\n                    continue\n                except discord.HTTPException:\n                    log.exception(f\"Exception while trying to retrieve member {key} from Discord.\")\n                    continue\n\n            revoke_time = Arrow.utcfromtimestamp(value)\n            log.debug(f\"Scheduling {member} ({member.id}) to have streaming permission revoked at {revoke_time}\")\n            self.scheduler.schedule_at(\n                revoke_time,\n                key,\n                self._revoke_streaming_permission(member)\n            )\n\n    @commands.command(aliases=(\"streaming\",))\n    @commands.has_any_role(*MODERATION_ROLES)\n    async def stream(self, ctx: commands.Context, member: discord.Member, duration: Expiry = None) -> None:\n        \"\"\"\n        Temporarily grant streaming permissions to a member for a given duration.\n\n        A unit of time should be appended to the duration.\n        Units (\u2217case-sensitive):\n        \\u2003`y` - years\n        \\u2003`m` - months\u2217\n        \\u2003`w` - weeks\n        \\u2003`d` - days\n        \\u2003`h` - hours\n        \\u2003`M` - minutes\u2217\n        \\u2003`s` - seconds\n\n        Alternatively, an ISO 8601 timestamp can be provided for the duration.\n        \"\"\"\n        log.trace(f\"Attempting to give temporary streaming permission to {member} ({member.id}).\")\n\n        if duration is None:\n            # Use default duration and convert back to datetime as Embed.timestamp doesn't support Arrow\n            duration = arrow.utcnow() + timedelta(minutes=VideoPermission.default_permission_duration)\n            duration = duration.datetime\n        elif duration.tzinfo is None:\n            # Make duration tz-aware.\n            # ISODateTime could already include tzinfo, this check is so it isn't overwritten.\n            duration.replace(tzinfo=timezone.utc)\n\n        # Check if the member already has streaming permission\n        already_allowed = any(Roles.video == role.id for role in member.roles)\n        if already_allowed:\n            await ctx.send(f\"{Emojis.cross_mark} {member.mention} can already stream.\")\n            log.debug(f\"{member} ({member.id}) already has permission to stream.\")\n            return\n\n        # Schedule task to remove streaming permission from Member and add it to task cache\n        self.scheduler.schedule_at(duration, member.id, self._revoke_streaming_permission(member))\n        await self.task_cache.set(member.id, duration.timestamp())\n\n        await member.add_roles(discord.Object(Roles.video), reason=\"Temporary streaming access granted\")\n\n        # Use embed as embed timestamps do timezone conversions.\n        embed = discord.Embed(\n            description=f\"{Emojis.check_mark} {member.mention} can now stream.\",\n            colour=Colours.soft_green\n        )\n        embed.set_footer(text=f\"Streaming permission has been given to {member} until\")\n        embed.timestamp = duration\n\n        # Mention in content as mentions in embeds don't ping\n        await ctx.send(content=member.mention, embed=embed)\n\n        # Convert here for nicer logging\n        revoke_time = format_infraction_with_duration(str(duration))\n        log.debug(f\"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.\")\n\n    @commands.command(aliases=(\"pstream\",))\n    @commands.has_any_role(*MODERATION_ROLES)\n    async def permanentstream(self, ctx: commands.Context, member: discord.Member) -> None:\n        \"\"\"Permanently grants the given member the permission to stream.\"\"\"\n        log.trace(f\"Attempting to give permanent streaming permission to {member} ({member.id}).\")\n\n        # Check if the member already has streaming permission\n        if any(Roles.video == role.id for role in member.roles):\n            if member.id in self.scheduler:\n                # Member has temp permission, so cancel the task to revoke later and delete from cache\n                self.scheduler.cancel(member.id)\n                await self.task_cache.delete(member.id)\n\n                await ctx.send(f\"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.\")\n                log.debug(\n                    f\"Successfully upgraded temporary streaming permission for {member} ({member.id}) to permanent.\"\n                )\n                return\n\n            await ctx.send(f\"{Emojis.cross_mark} This member can already stream.\")\n            log.debug(f\"{member} ({member.id}) already had permanent streaming permission.\")\n            return\n\n        await member.add_roles(discord.Object(Roles.video), reason=\"Permanent streaming access granted\")\n        await ctx.send(f\"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.\")\n        log.debug(f\"Successfully gave {member} ({member.id}) permanent streaming permission.\")\n\n    @commands.command(aliases=(\"unstream\", \"rstream\"))\n    @commands.has_any_role(*MODERATION_ROLES)\n    async def revokestream(self, ctx: commands.Context, member: discord.Member) -> None:\n        \"\"\"Revoke the permission to stream from the given member.\"\"\"\n        log.trace(f\"Attempting to remove streaming permission from {member} ({member.id}).\")\n\n        # Check if the member already has streaming permission\n        if any(Roles.video == role.id for role in member.roles):\n            if member.id in self.scheduler:\n                # Member has temp permission, so cancel the task to revoke later and delete from cache\n                self.scheduler.cancel(member.id)\n                await self.task_cache.delete(member.id)\n            await self._revoke_streaming_permission(member)\n\n            await ctx.send(f\"{Emojis.check_mark} Revoked the permission to stream from {member.mention}.\")\n            log.debug(f\"Successfully revoked streaming permission from {member} ({member.id}).\")\n            return\n\n        await ctx.send(f\"{Emojis.cross_mark} This member doesn't have video permissions to remove!\")\n        log.debug(f\"{member} ({member.id}) didn't have the streaming permission to remove!\")\n\n    @commands.command(aliases=('lstream',))\n    @commands.has_any_role(*MODERATION_ROLES)\n    async def liststream(self, ctx: commands.Context) -> None:\n        \"\"\"Lists all non-staff users who have permission to stream.\"\"\"\n        non_staff_members_with_stream = [\n            member\n            for member in ctx.guild.get_role(Roles.video).members\n            if not any(role.id in STAFF_ROLES for role in member.roles)\n        ]\n\n        # List of tuples (UtcPosixTimestamp, str)\n        # So that the list can be sorted on the UtcPosixTimestamp before the message is passed to the paginator.\n        streamer_info = []\n        for member in non_staff_members_with_stream:\n            if revoke_time := await self.task_cache.get(member.id):\n                # Member only has temporary streaming perms\n                revoke_delta = Arrow.utcfromtimestamp(revoke_time).humanize()\n                message = f\"{member.mention} will have stream permissions revoked {revoke_delta}.\"\n            else:\n                message = f\"{member.mention} has permanent streaming permissions.\"\n\n            # If revoke_time is None use max timestamp to force sort to put them at the end\n            streamer_info.append(\n                (revoke_time or Arrow.max.timestamp(), message)\n            )\n\n        if streamer_info:\n            # Sort based on duration left of streaming perms\n            streamer_info.sort(key=itemgetter(0))\n\n            # Only output the message in the pagination\n            lines = [line[1] for line in streamer_info]\n            embed = discord.Embed(\n                title=f\"Members with streaming permission (`{len(lines)}` total)\",\n                colour=Colours.soft_green\n            )\n            await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)\n        else:\n            await ctx.send(\"No members with stream permissions found.\")\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Loads the Stream cog.\"\"\"\n    bot.add_cog(Stream(bot))\n", "path": "bot/exts/moderation/stream.py"}], "after_files": [{"content": "import logging\nfrom datetime import timedelta, timezone\nfrom operator import itemgetter\n\nimport arrow\nimport discord\nfrom arrow import Arrow\nfrom async_rediscache import RedisCache\nfrom discord.ext import commands\n\nfrom bot.bot import Bot\nfrom bot.constants import Colours, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES, VideoPermission\nfrom bot.converters import Expiry\nfrom bot.pagination import LinePaginator\nfrom bot.utils.scheduling import Scheduler\nfrom bot.utils.time import format_infraction_with_duration\n\nlog = logging.getLogger(__name__)\n\n\nclass Stream(commands.Cog):\n    \"\"\"Grant and revoke streaming permissions from members.\"\"\"\n\n    # Stores tasks to remove streaming permission\n    # RedisCache[discord.Member.id, UtcPosixTimestamp]\n    task_cache = RedisCache()\n\n    def __init__(self, bot: Bot):\n        self.bot = bot\n        self.scheduler = Scheduler(self.__class__.__name__)\n        self.reload_task = self.bot.loop.create_task(self._reload_tasks_from_redis())\n\n    def cog_unload(self) -> None:\n        \"\"\"Cancel all scheduled tasks.\"\"\"\n        self.reload_task.cancel()\n        self.reload_task.add_done_callback(lambda _: self.scheduler.cancel_all())\n\n    async def _revoke_streaming_permission(self, member: discord.Member) -> None:\n        \"\"\"Remove the streaming permission from the given Member.\"\"\"\n        await self.task_cache.delete(member.id)\n        await member.remove_roles(discord.Object(Roles.video), reason=\"Streaming access revoked\")\n\n    async def _reload_tasks_from_redis(self) -> None:\n        \"\"\"Reload outstanding tasks from redis on startup, delete the task if the member has since left the server.\"\"\"\n        await self.bot.wait_until_guild_available()\n        items = await self.task_cache.items()\n        for key, value in items:\n            member = self.bot.get_guild(Guild.id).get_member(key)\n\n            if not member:\n                # Member isn't found in the cache\n                try:\n                    member = await self.bot.get_guild(Guild.id).fetch_member(key)\n                except discord.errors.NotFound:\n                    log.debug(\n                        f\"Member {key} left the guild before we could schedule \"\n                        \"the revoking of their streaming permissions.\"\n                    )\n                    await self.task_cache.delete(key)\n                    continue\n                except discord.HTTPException:\n                    log.exception(f\"Exception while trying to retrieve member {key} from Discord.\")\n                    continue\n\n            revoke_time = Arrow.utcfromtimestamp(value)\n            log.debug(f\"Scheduling {member} ({member.id}) to have streaming permission revoked at {revoke_time}\")\n            self.scheduler.schedule_at(\n                revoke_time,\n                key,\n                self._revoke_streaming_permission(member)\n            )\n\n    async def _suspend_stream(self, ctx: commands.Context, member: discord.Member) -> None:\n        \"\"\"Suspend a member's stream.\"\"\"\n        await self.bot.wait_until_guild_available()\n        voice_state = member.voice\n\n        if not voice_state:\n            return\n\n        # If the user is streaming.\n        if voice_state.self_stream:\n            # End user's stream by moving them to AFK voice channel and back.\n            original_vc = voice_state.channel\n            await member.move_to(ctx.guild.afk_channel)\n            await member.move_to(original_vc)\n\n            # Notify.\n            await ctx.send(f\"{member.mention}'s stream has been suspended!\")\n            log.debug(f\"Successfully suspended stream from {member} ({member.id}).\")\n            return\n\n        log.debug(f\"No stream found to suspend from {member} ({member.id}).\")\n\n    @commands.command(aliases=(\"streaming\",))\n    @commands.has_any_role(*MODERATION_ROLES)\n    async def stream(self, ctx: commands.Context, member: discord.Member, duration: Expiry = None) -> None:\n        \"\"\"\n        Temporarily grant streaming permissions to a member for a given duration.\n\n        A unit of time should be appended to the duration.\n        Units (\u2217case-sensitive):\n        \\u2003`y` - years\n        \\u2003`m` - months\u2217\n        \\u2003`w` - weeks\n        \\u2003`d` - days\n        \\u2003`h` - hours\n        \\u2003`M` - minutes\u2217\n        \\u2003`s` - seconds\n\n        Alternatively, an ISO 8601 timestamp can be provided for the duration.\n        \"\"\"\n        log.trace(f\"Attempting to give temporary streaming permission to {member} ({member.id}).\")\n\n        if duration is None:\n            # Use default duration and convert back to datetime as Embed.timestamp doesn't support Arrow\n            duration = arrow.utcnow() + timedelta(minutes=VideoPermission.default_permission_duration)\n            duration = duration.datetime\n        elif duration.tzinfo is None:\n            # Make duration tz-aware.\n            # ISODateTime could already include tzinfo, this check is so it isn't overwritten.\n            duration.replace(tzinfo=timezone.utc)\n\n        # Check if the member already has streaming permission\n        already_allowed = any(Roles.video == role.id for role in member.roles)\n        if already_allowed:\n            await ctx.send(f\"{Emojis.cross_mark} {member.mention} can already stream.\")\n            log.debug(f\"{member} ({member.id}) already has permission to stream.\")\n            return\n\n        # Schedule task to remove streaming permission from Member and add it to task cache\n        self.scheduler.schedule_at(duration, member.id, self._revoke_streaming_permission(member))\n        await self.task_cache.set(member.id, duration.timestamp())\n\n        await member.add_roles(discord.Object(Roles.video), reason=\"Temporary streaming access granted\")\n\n        # Use embed as embed timestamps do timezone conversions.\n        embed = discord.Embed(\n            description=f\"{Emojis.check_mark} {member.mention} can now stream.\",\n            colour=Colours.soft_green\n        )\n        embed.set_footer(text=f\"Streaming permission has been given to {member} until\")\n        embed.timestamp = duration\n\n        # Mention in content as mentions in embeds don't ping\n        await ctx.send(content=member.mention, embed=embed)\n\n        # Convert here for nicer logging\n        revoke_time = format_infraction_with_duration(str(duration))\n        log.debug(f\"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.\")\n\n    @commands.command(aliases=(\"pstream\",))\n    @commands.has_any_role(*MODERATION_ROLES)\n    async def permanentstream(self, ctx: commands.Context, member: discord.Member) -> None:\n        \"\"\"Permanently grants the given member the permission to stream.\"\"\"\n        log.trace(f\"Attempting to give permanent streaming permission to {member} ({member.id}).\")\n\n        # Check if the member already has streaming permission\n        if any(Roles.video == role.id for role in member.roles):\n            if member.id in self.scheduler:\n                # Member has temp permission, so cancel the task to revoke later and delete from cache\n                self.scheduler.cancel(member.id)\n                await self.task_cache.delete(member.id)\n\n                await ctx.send(f\"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.\")\n                log.debug(\n                    f\"Successfully upgraded temporary streaming permission for {member} ({member.id}) to permanent.\"\n                )\n                return\n\n            await ctx.send(f\"{Emojis.cross_mark} This member can already stream.\")\n            log.debug(f\"{member} ({member.id}) already had permanent streaming permission.\")\n            return\n\n        await member.add_roles(discord.Object(Roles.video), reason=\"Permanent streaming access granted\")\n        await ctx.send(f\"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.\")\n        log.debug(f\"Successfully gave {member} ({member.id}) permanent streaming permission.\")\n\n    @commands.command(aliases=(\"unstream\", \"rstream\"))\n    @commands.has_any_role(*MODERATION_ROLES)\n    async def revokestream(self, ctx: commands.Context, member: discord.Member) -> None:\n        \"\"\"Revoke the permission to stream from the given member.\"\"\"\n        log.trace(f\"Attempting to remove streaming permission from {member} ({member.id}).\")\n\n        # Check if the member already has streaming permission\n        if any(Roles.video == role.id for role in member.roles):\n            if member.id in self.scheduler:\n                # Member has temp permission, so cancel the task to revoke later and delete from cache\n                self.scheduler.cancel(member.id)\n                await self.task_cache.delete(member.id)\n            await self._revoke_streaming_permission(member)\n\n            await ctx.send(f\"{Emojis.check_mark} Revoked the permission to stream from {member.mention}.\")\n            log.debug(f\"Successfully revoked streaming permission from {member} ({member.id}).\")\n\n        else:\n            await ctx.send(f\"{Emojis.cross_mark} This member doesn't have video permissions to remove!\")\n            log.debug(f\"{member} ({member.id}) didn't have the streaming permission to remove!\")\n\n        await self._suspend_stream(ctx, member)\n\n    @commands.command(aliases=('lstream',))\n    @commands.has_any_role(*MODERATION_ROLES)\n    async def liststream(self, ctx: commands.Context) -> None:\n        \"\"\"Lists all non-staff users who have permission to stream.\"\"\"\n        non_staff_members_with_stream = [\n            member\n            for member in ctx.guild.get_role(Roles.video).members\n            if not any(role.id in STAFF_ROLES for role in member.roles)\n        ]\n\n        # List of tuples (UtcPosixTimestamp, str)\n        # So that the list can be sorted on the UtcPosixTimestamp before the message is passed to the paginator.\n        streamer_info = []\n        for member in non_staff_members_with_stream:\n            if revoke_time := await self.task_cache.get(member.id):\n                # Member only has temporary streaming perms\n                revoke_delta = Arrow.utcfromtimestamp(revoke_time).humanize()\n                message = f\"{member.mention} will have stream permissions revoked {revoke_delta}.\"\n            else:\n                message = f\"{member.mention} has permanent streaming permissions.\"\n\n            # If revoke_time is None use max timestamp to force sort to put them at the end\n            streamer_info.append(\n                (revoke_time or Arrow.max.timestamp(), message)\n            )\n\n        if streamer_info:\n            # Sort based on duration left of streaming perms\n            streamer_info.sort(key=itemgetter(0))\n\n            # Only output the message in the pagination\n            lines = [line[1] for line in streamer_info]\n            embed = discord.Embed(\n                title=f\"Members with streaming permission (`{len(lines)}` total)\",\n                colour=Colours.soft_green\n            )\n            await LinePaginator.paginate(lines, ctx, embed, max_size=400, empty=False)\n        else:\n            await ctx.send(\"No members with stream permissions found.\")\n\n\ndef setup(bot: Bot) -> None:\n    \"\"\"Loads the Stream cog.\"\"\"\n    bot.add_cog(Stream(bot))\n", "path": "bot/exts/moderation/stream.py"}]}