aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Matteo Bertucci <[email protected]>2021-04-11 15:56:00 +0200
committerGravatar GitHub <[email protected]>2021-04-11 15:56:00 +0200
commit11fc6e977c9489f027d50e5e09b0e6616fae88cc (patch)
tree2f31064e3fc461f8dcf3f22bc40a2cc1dc5f823a
parentRecruitment: Don't use emoji literals (diff)
parentMerge pull request #1511 from onerandomusername/defcon-voice-shutdown (diff)
Merge branch 'main' into tp-get_review-command
-rw-r--r--bot/constants.py7
-rw-r--r--bot/exts/moderation/defcon.py4
-rw-r--r--bot/exts/moderation/stream.py179
-rw-r--r--bot/resources/tags/ytdl.md8
-rw-r--r--bot/utils/scheduling.py8
-rw-r--r--config-default.yml7
6 files changed, 205 insertions, 8 deletions
diff --git a/bot/constants.py b/bot/constants.py
index 4884a1278..547a94a0b 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -482,6 +482,7 @@ class Roles(metaclass=YAMLGetter):
python_community: int
sprinters: int
voice_verified: int
+ video: int
admins: int
core_developers: int
@@ -661,6 +662,12 @@ class Event(Enum):
voice_state_update = "voice_state_update"
+class VideoPermission(metaclass=YAMLGetter):
+ section = "video_permission"
+
+ default_permission_duration: int
+
+
# Debug mode
DEBUG_MODE = 'local' in os.environ.get("SITE_URL", "local")
diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py
index bab95405c..dfb1afd19 100644
--- a/bot/exts/moderation/defcon.py
+++ b/bot/exts/moderation/defcon.py
@@ -181,7 +181,7 @@ class Defcon(Cog):
role = ctx.guild.default_role
permissions = role.permissions
- permissions.update(send_messages=False, add_reactions=False)
+ permissions.update(send_messages=False, add_reactions=False, connect=False)
await role.edit(reason="DEFCON shutdown", permissions=permissions)
await ctx.send(f"{Action.SERVER_SHUTDOWN.value.emoji} Server shut down.")
@@ -192,7 +192,7 @@ class Defcon(Cog):
role = ctx.guild.default_role
permissions = role.permissions
- permissions.update(send_messages=True, add_reactions=True)
+ permissions.update(send_messages=True, add_reactions=True, connect=True)
await role.edit(reason="DEFCON unshutdown", permissions=permissions)
await ctx.send(f"{Action.SERVER_OPEN.value.emoji} Server reopened.")
diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py
new file mode 100644
index 000000000..12e195172
--- /dev/null
+++ b/bot/exts/moderation/stream.py
@@ -0,0 +1,179 @@
+import logging
+from datetime import timedelta, timezone
+
+import arrow
+import discord
+from arrow import Arrow
+from async_rediscache import RedisCache
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Colours, Emojis, Guild, Roles, STAFF_ROLES, VideoPermission
+from bot.converters import Expiry
+from bot.utils.scheduling import Scheduler
+from bot.utils.time import format_infraction_with_duration
+
+log = logging.getLogger(__name__)
+
+
+class Stream(commands.Cog):
+ """Grant and revoke streaming permissions from members."""
+
+ # Stores tasks to remove streaming permission
+ # RedisCache[discord.Member.id, UtcPosixTimestamp]
+ task_cache = RedisCache()
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+ self.scheduler = Scheduler(self.__class__.__name__)
+ self.reload_task = self.bot.loop.create_task(self._reload_tasks_from_redis())
+
+ def cog_unload(self) -> None:
+ """Cancel all scheduled tasks."""
+ self.reload_task.cancel()
+ self.reload_task.add_done_callback(lambda _: self.scheduler.cancel_all())
+
+ async def _revoke_streaming_permission(self, member: discord.Member) -> None:
+ """Remove the streaming permission from the given Member."""
+ await self.task_cache.delete(member.id)
+ await member.remove_roles(discord.Object(Roles.video), reason="Streaming access revoked")
+
+ async def _reload_tasks_from_redis(self) -> None:
+ """Reload outstanding tasks from redis on startup, delete the task if the member has since left the server."""
+ await self.bot.wait_until_guild_available()
+ items = await self.task_cache.items()
+ for key, value in items:
+ member = self.bot.get_guild(Guild.id).get_member(key)
+
+ if not member:
+ # Member isn't found in the cache
+ try:
+ member = await self.bot.get_guild(Guild.id).fetch_member(key)
+ except discord.errors.NotFound:
+ log.debug(
+ f"Member {key} left the guild before we could schedule "
+ "the revoking of their streaming permissions."
+ )
+ await self.task_cache.delete(key)
+ continue
+ except discord.HTTPException:
+ log.exception(f"Exception while trying to retrieve member {key} from Discord.")
+ continue
+
+ revoke_time = Arrow.utcfromtimestamp(value)
+ log.debug(f"Scheduling {member} ({member.id}) to have streaming permission revoked at {revoke_time}")
+ self.scheduler.schedule_at(
+ revoke_time,
+ key,
+ self._revoke_streaming_permission(member)
+ )
+
+ @commands.command(aliases=("streaming",))
+ @commands.has_any_role(*STAFF_ROLES)
+ async def stream(self, ctx: commands.Context, member: discord.Member, duration: Expiry = None) -> None:
+ """
+ Temporarily grant streaming permissions to a member for a given duration.
+
+ A unit of time should be appended to the duration.
+ Units (∗case-sensitive):
+ \u2003`y` - years
+ \u2003`m` - months∗
+ \u2003`w` - weeks
+ \u2003`d` - days
+ \u2003`h` - hours
+ \u2003`M` - minutes∗
+ \u2003`s` - seconds
+
+ Alternatively, an ISO 8601 timestamp can be provided for the duration.
+ """
+ log.trace(f"Attempting to give temporary streaming permission to {member} ({member.id}).")
+
+ if duration is None:
+ # Use default duration and convert back to datetime as Embed.timestamp doesn't support Arrow
+ duration = arrow.utcnow() + timedelta(minutes=VideoPermission.default_permission_duration)
+ duration = duration.datetime
+ elif duration.tzinfo is None:
+ # Make duration tz-aware.
+ # ISODateTime could already include tzinfo, this check is so it isn't overwritten.
+ duration.replace(tzinfo=timezone.utc)
+
+ # Check if the member already has streaming permission
+ already_allowed = any(Roles.video == role.id for role in member.roles)
+ if already_allowed:
+ await ctx.send(f"{Emojis.cross_mark} {member.mention} can already stream.")
+ log.debug(f"{member} ({member.id}) already has permission to stream.")
+ return
+
+ # Schedule task to remove streaming permission from Member and add it to task cache
+ self.scheduler.schedule_at(duration, member.id, self._revoke_streaming_permission(member))
+ await self.task_cache.set(member.id, duration.timestamp())
+
+ await member.add_roles(discord.Object(Roles.video), reason="Temporary streaming access granted")
+
+ # Use embed as embed timestamps do timezone conversions.
+ embed = discord.Embed(
+ description=f"{Emojis.check_mark} {member.mention} can now stream.",
+ colour=Colours.soft_green
+ )
+ embed.set_footer(text=f"Streaming permission has been given to {member} until")
+ embed.timestamp = duration
+
+ # Mention in content as mentions in embeds don't ping
+ await ctx.send(content=member.mention, embed=embed)
+
+ # Convert here for nicer logging
+ revoke_time = format_infraction_with_duration(str(duration))
+ log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.")
+
+ @commands.command(aliases=("pstream",))
+ @commands.has_any_role(*STAFF_ROLES)
+ async def permanentstream(self, ctx: commands.Context, member: discord.Member) -> None:
+ """Permanently grants the given member the permission to stream."""
+ log.trace(f"Attempting to give permanent streaming permission to {member} ({member.id}).")
+
+ # Check if the member already has streaming permission
+ if any(Roles.video == role.id for role in member.roles):
+ if member.id in self.scheduler:
+ # Member has temp permission, so cancel the task to revoke later and delete from cache
+ self.scheduler.cancel(member.id)
+ await self.task_cache.delete(member.id)
+
+ await ctx.send(f"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.")
+ log.debug(
+ f"Successfully upgraded temporary streaming permission for {member} ({member.id}) to permanent."
+ )
+ return
+
+ await ctx.send(f"{Emojis.cross_mark} This member can already stream.")
+ log.debug(f"{member} ({member.id}) already had permanent streaming permission.")
+ return
+
+ await member.add_roles(discord.Object(Roles.video), reason="Permanent streaming access granted")
+ await ctx.send(f"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.")
+ log.debug(f"Successfully gave {member} ({member.id}) permanent streaming permission.")
+
+ @commands.command(aliases=("unstream", "rstream"))
+ @commands.has_any_role(*STAFF_ROLES)
+ async def revokestream(self, ctx: commands.Context, member: discord.Member) -> None:
+ """Revoke the permission to stream from the given member."""
+ log.trace(f"Attempting to remove streaming permission from {member} ({member.id}).")
+
+ # Check if the member already has streaming permission
+ if any(Roles.video == role.id for role in member.roles):
+ if member.id in self.scheduler:
+ # Member has temp permission, so cancel the task to revoke later and delete from cache
+ self.scheduler.cancel(member.id)
+ await self.task_cache.delete(member.id)
+ await self._revoke_streaming_permission(member)
+
+ 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!")
+
+
+def setup(bot: Bot) -> None:
+ """Loads the Stream cog."""
+ bot.add_cog(Stream(bot))
diff --git a/bot/resources/tags/ytdl.md b/bot/resources/tags/ytdl.md
index e34ecff44..df28024a0 100644
--- a/bot/resources/tags/ytdl.md
+++ b/bot/resources/tags/ytdl.md
@@ -1,12 +1,12 @@
Per [PyDis' Rule 5](https://pythondiscord.com/pages/rules), we are unable to assist with questions related to youtube-dl, commonly used by Discord bots to stream audio, as its use violates YouTube's Terms of Service.
-For reference, this usage is covered by the following clauses in [YouTube's TOS](https://www.youtube.com/static?template=terms), as of 2019-07-22:
+For reference, this usage is covered by the following clauses in [YouTube's TOS](https://www.youtube.com/static?gl=GB&template=terms), as of 2021-03-17:
```
-The following restrictions apply to your use of the Service. You are not allowed to:
+The following restrictions apply to your use of the Service. You are not allowed to:
-1. access, reproduce, download, distribute, transmit, broadcast, display, sell, license, alter, modify or otherwise use any part of the Service or any Content except: (a) as specifically permitted by the Service; (b) with prior written permission from YouTube and, if applicable, the respective rights holders; or (c) as permitted by applicable law;
+1. access, reproduce, download, distribute, transmit, broadcast, display, sell, license, alter, modify or otherwise use any part of the Service or any Content except: (a) as specifically permitted by the Service; (b) with prior written permission from YouTube and, if applicable, the respective rights holders; or (c) as permitted by applicable law;
-3. access the Service using any automated means (such as robots, botnets or scrapers) except: (a) in the case of public search engines, in accordance with YouTube’s robots.txt file; (b) with YouTube’s prior written permission; or (c) as permitted by applicable law;
+3. access the Service using any automated means (such as robots, botnets or scrapers) except: (a) in the case of public search engines, in accordance with YouTube’s robots.txt file; (b) with YouTube’s prior written permission; or (c) as permitted by applicable law;
9. use the Service to view or listen to Content other than for personal, non-commercial use (for example, you may not publicly screen videos or stream music from the Service)
```
diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py
index 4dd036e4f..6843bae88 100644
--- a/bot/utils/scheduling.py
+++ b/bot/utils/scheduling.py
@@ -59,14 +59,18 @@ class Scheduler:
def schedule_at(self, time: datetime, task_id: t.Hashable, coroutine: t.Coroutine) -> None:
"""
- Schedule `coroutine` to be executed at the given naïve UTC `time`.
+ Schedule `coroutine` to be executed at the given `time`.
+
+ If `time` is timezone aware, then use that timezone to calculate now() when subtracting.
+ If `time` is naïve, then use UTC.
If `time` is in the past, schedule `coroutine` immediately.
If a task with `task_id` already exists, close `coroutine` instead of scheduling it. This
prevents unawaited coroutine warnings. Don't pass a coroutine that'll be re-used elsewhere.
"""
- delay = (time - datetime.utcnow()).total_seconds()
+ now_datetime = datetime.now(time.tzinfo) if time.tzinfo else datetime.utcnow()
+ delay = (time - now_datetime).total_seconds()
if delay > 0:
coroutine = self._await_later(delay, task_id, coroutine)
diff --git a/config-default.yml b/config-default.yml
index b89f1505e..9b07d026d 100644
--- a/config-default.yml
+++ b/config-default.yml
@@ -267,6 +267,9 @@ guild:
jammers: 737249140966162473
team_leaders: 737250302834638889
+ # Streaming
+ video: 764245844798079016
+
moderation_roles:
- *ADMINS_ROLE
- *MODS_ROLE
@@ -542,3 +545,7 @@ branding:
config:
required_keys: ['bot.token']
+
+
+video_permission:
+ default_permission_duration: 5 # Default duration for stream command in minutes