diff options
author | 2021-04-11 15:56:00 +0200 | |
---|---|---|
committer | 2021-04-11 15:56:00 +0200 | |
commit | 11fc6e977c9489f027d50e5e09b0e6616fae88cc (patch) | |
tree | 2f31064e3fc461f8dcf3f22bc40a2cc1dc5f823a | |
parent | Recruitment: Don't use emoji literals (diff) | |
parent | Merge pull request #1511 from onerandomusername/defcon-voice-shutdown (diff) |
Merge branch 'main' into tp-get_review-command
-rw-r--r-- | bot/constants.py | 7 | ||||
-rw-r--r-- | bot/exts/moderation/defcon.py | 4 | ||||
-rw-r--r-- | bot/exts/moderation/stream.py | 179 | ||||
-rw-r--r-- | bot/resources/tags/ytdl.md | 8 | ||||
-rw-r--r-- | bot/utils/scheduling.py | 8 | ||||
-rw-r--r-- | config-default.yml | 7 |
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 |