diff options
| -rw-r--r-- | Pipfile.lock | 68 | ||||
| -rw-r--r-- | bot/constants.py | 27 | ||||
| -rw-r--r-- | bot/exts/moderation/stream.py | 106 | ||||
| -rw-r--r-- | tests/bot/exts/moderation/test_stream.py | 56 |
4 files changed, 72 insertions, 185 deletions
diff --git a/Pipfile.lock b/Pipfile.lock index 541db1627..25fcab4b1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -187,6 +187,7 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==0.4.4" }, @@ -231,10 +232,10 @@ }, "fakeredis": { "hashes": [ - "sha256:8070b7fce16f828beaef2c757a4354af91698685d5232404f1aeeb233529c7a5", - "sha256:f8c8ea764d7b6fd801e7f5486e3edd32ca991d506186f1923a01fc072e33c271" + "sha256:01cb47d2286825a171fb49c0e445b1fa9307087e07cbb3d027ea10dbff108b6a", + "sha256:2c6041cf0225889bc403f3949838b2c53470a95a9e2d4272422937786f5f8f73" ], - "version": "==1.4.4" + "version": "==1.4.5" }, "feedparser": { "hashes": [ @@ -538,6 +539,15 @@ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, + "pyreadline": { + "hashes": [ + "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1", + "sha256:65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e", + "sha256:9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b" + ], + "markers": "sys_platform == 'win32'", + "version": "==2.1" + }, "python-dateutil": { "hashes": [ "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", @@ -555,18 +565,18 @@ }, "pyyaml": { "hashes": [ - "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", - "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", - "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", - "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", - "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", - "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", - "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], "index": "pypi", @@ -846,11 +856,11 @@ }, "flake8-bugbear": { "hashes": [ - "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63", - "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162" + "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538", + "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703" ], "index": "pypi", - "version": "==20.1.4" + "version": "==20.11.1" }, "flake8-docstrings": { "hashes": [ @@ -900,11 +910,11 @@ }, "identify": { "hashes": [ - "sha256:5dd84ac64a9a115b8e0b27d1756b244b882ad264c3c423f42af8235a6e71ca12", - "sha256:c9504ba6a043ee2db0a9d69e43246bc138034895f6338d5aed1b41e4a73b1513" + "sha256:943cd299ac7f5715fcb3f684e2fc1594c1e0f22a90d15398e5888143bd4144b5", + "sha256:cc86e6a9a390879dcc2976cef169dd9cc48843ed70b7380f321d1b118163c60e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.5.9" + "version": "==1.5.10" }, "idna": { "hashes": [ @@ -938,11 +948,11 @@ }, "pre-commit": { "hashes": [ - "sha256:22e6aa3bd571debb01eb7d34483f11c01b65237be4eebbf30c3d4fb65762d315", - "sha256:905ebc9b534b991baec87e934431f2d0606ba27f2b90f7f652985f5a5b8b6ae6" + "sha256:4aee0db4808fa48d2458cedd5b9a084ef24dda1a0fa504432a11977a4d1cfd0a", + "sha256:b2d106d51c6ba6217e859d81774aae33fd825fe7de0dcf0c46e2586333d7a92e" ], "index": "pypi", - "version": "==2.8.2" + "version": "==2.9.0" }, "pycodestyle": { "hashes": [ @@ -970,18 +980,18 @@ }, "pyyaml": { "hashes": [ - "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", - "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", - "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", - "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", - "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", - "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", - "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], "index": "pypi", @@ -1028,11 +1038,11 @@ }, "virtualenv": { "hashes": [ - "sha256:b0011228208944ce71052987437d3843e05690b2f23d1c7da4263fde104c97a2", - "sha256:b8d6110f493af256a40d65e29846c69340a947669eec8ce784fcf3dd3af28380" + "sha256:07cff122e9d343140366055f31be4dcd61fd598c69d11cd33a9d9c8df4546dd7", + "sha256:e0aac7525e880a429764cefd3aaaff54afb5d9f25c82627563603f5d7de5a6e5" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.1.0" + "version": "==20.2.1" } } } diff --git a/bot/constants.py b/bot/constants.py index 33ed29c39..dca83e7ab 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -705,30 +705,3 @@ ERROR_REPLIES = [ "Noooooo!!", "I can't believe you've done this", ] - -# TIME_FORMATS defines aliases and multipliers for time formats -# key is a standard time unit name like second ,year, decade etc. -# mul is a multiplier where duration of said time unit * multiplier = time in seconds -# eg. 1 day = 1 * multiplier seconds, so mul = 86400 -TIME_FORMATS = { - "second": { - "aliases": ("s", "sec", "seconds", "secs"), - "mul": 1 - }, - "minute": { - "aliases": ("m", "min", "mins", "minutes"), - "mul": 60 - }, - "hour": { - "aliases": ("h", "hr", "hrs", "hours"), - "mul": 3600 - }, - "day": { - "aliases": ("d", "days"), - "mul": 86400 - }, - "year": { - "aliases": ("yr", "yrs", "years", "y"), - "mul": 31536000 - } -} diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 7dd72a95b..0fc004d75 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -1,11 +1,11 @@ -import time - import discord -from async_rediscache import RedisCache -from discord.ext import commands, tasks +from discord.ext import commands from bot.bot import Bot -from bot.constants import Guild, Roles, STAFF_ROLES, TIME_FORMATS, Emojis +from bot.constants import Emojis, Roles, STAFF_ROLES +from bot.converters import Expiry +from bot.utils.scheduling import Scheduler +from bot.utils.time import format_infraction_with_duration # Constant error messages TIME_FORMAT_NOT_VALID = f"{Emojis.cross_mark}Please specify a valid time format ex. 10h or 1day." @@ -14,37 +14,17 @@ USER_ALREADY_ALLOWED_TO_STREAM = f"{Emojis.cross_mark}This user can already stre USER_ALREADY_NOT_ALLOWED_TO_STREAM = f"{Emojis.cross_mark}This user already can't stream." -# FORMATS holds a combined list of all allowed time units -# made from TIME_FORMATS constant -FORMATS = [] -for key, entry in TIME_FORMATS.items(): - FORMATS.extend(entry["aliases"]) - FORMATS.append(key) - - class Stream(commands.Cog): """Grant and revoke streaming permissions from users.""" - # Data cache storing userid to unix_time relation - # user id is used to get member who's streaming permission need to be revoked after some time - # unix_time is a time when user's streaming permission needs tp be revoked in unix time notation - user_cache = RedisCache() - def __init__(self, bot: Bot): self.bot = bot - self.remove_permissions.start() - self.guild_static = None + self.scheduler = Scheduler(self.__class__.__name__) @staticmethod - def _link_from_alias(time_format: str) -> (dict, str): - """Get TIME_FORMATS key and entry by time format or any of its aliases.""" - for format_key, val in TIME_FORMATS.items(): - if format_key == time_format or time_format in val["aliases"]: - return TIME_FORMATS[format_key], format_key - - def _parse_time_to_seconds(self, duration: int, time_format: str) -> int: - """Get time in seconds from duration and time format.""" - return duration * self._link_from_alias(time_format)[0]["mul"] + async def _remove_streaming_permission(schedule_user: discord.Member) -> None: + """Remove streaming permission from Member""" + await schedule_user.remove_roles(discord.Object(Roles.video), reason="Temporary streaming access revoked") @commands.command(aliases=("streaming",)) @commands.has_any_role(*STAFF_ROLES) @@ -52,68 +32,48 @@ class Stream(commands.Cog): self, ctx: commands.Context, user: discord.Member, - duration: int = 1, - time_format: str = "h", + duration: Expiry, *_ ) -> None: """ - Stream handles <prefix>stream command. - - argument user - required user mention, any errors should be handled by upper level handler - duration - int must be higher than 0 - defaults to 1 - time_format - str defining what time unit you want to use, must be any of FORMATS - defaults to h - Command give user permission to stream and takes it away after provided duration + Temporarily grant streaming permissions to a user 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. """ - # Time can't be negative lol - if duration <= 0: - await ctx.send(TIME_LESS_EQ_0) - return - - # Check if time_format argument is a valid time format - # eg. d, day etc are aliases for day time format - if time_format not in FORMATS: - await ctx.send(TIME_FORMAT_NOT_VALID) - return - # Check if user already has streaming permission already_allowed = any(Roles.video == role.id for role in user.roles) if already_allowed: await ctx.send(USER_ALREADY_ALLOWED_TO_STREAM) return - # Set user id - time in redis cache and add streaming permission role - await self.user_cache.set(user.id, time.time() + self._parse_time_to_seconds(duration, time_format)) + # Schedule task to remove streaming permission from Member + self.scheduler.schedule_at(duration, user.id, self._remove_streaming_permission(user)) await user.add_roles(discord.Object(Roles.video), reason="Temporary streaming access granted") - await ctx.send(f"{Emojis.check_mark}{user.mention} can now stream for " - f"{duration} {self._link_from_alias(time_format)[1]}/s.") - - @tasks.loop(seconds=30) - async def remove_permissions(self) -> None: - """Background loop for removing streaming permission.""" - all_entries = await self.user_cache.items() - for user_id, delete_time in all_entries: - if time.time() > delete_time: - member = self.guild_static.fetch_memeber(user_id) - if member: - await member.remove_roles(discord.Object(Roles.video), reason="Temporary streaming access revoked") - await self.user_cache.pop(user_id) - - @remove_permissions.before_loop - async def await_ready(self) -> None: - """Wait for bot to be ready before starting remove_permissions loop and get guild by id.""" - await self.bot.wait_until_ready() - self.guild_static = self.bot.get_guild(Guild.id) + await ctx.send(f"{Emojis.check_mark}{user.mention} can now stream until " + f"{format_infraction_with_duration(str(duration))}.") @commands.command(aliases=("unstream", )) @commands.has_any_role(*STAFF_ROLES) async def revokestream( self, ctx: commands.Context, - user: discord.Member = None + user: discord.Member ) -> None: - """Revoke streaming permissions from a user.""" - not_allowed = not any(Roles.video == role.id for role in user.roles) - if not_allowed: + """Take away streaming permission from a user""" + # Check if user has the streaming permission to begin with + allowed = any(Roles.video == role.id for role in user.roles) + if allowed: + # Cancel scheduled task to take away streaming permission to avoid errors + if user.id in self.scheduler: + self.scheduler.cancel(user.id) await user.remove_roles(discord.Object(Roles.video)) await ctx.send(f"{Emojis.check_mark}Streaming permission taken from {user.display_name}") else: diff --git a/tests/bot/exts/moderation/test_stream.py b/tests/bot/exts/moderation/test_stream.py index 467c373aa..15956a9de 100644 --- a/tests/bot/exts/moderation/test_stream.py +++ b/tests/bot/exts/moderation/test_stream.py @@ -1,28 +1,10 @@ -import asyncio import unittest -from async_rediscache import RedisSession from bot.constants import Roles from bot.exts.moderation.stream import Stream from tests.helpers import MockBot, MockMember, MockRole -redis_session = None -redis_loop = asyncio.get_event_loop() - - -def setUpModule(): # noqa: N802 - """Create and connect to the fakeredis session.""" - global redis_session - redis_session = RedisSession(use_fakeredis=True) - redis_loop.run_until_complete(redis_session.connect()) - - -def tearDownModule(): # noqa: N802 - """Close the fakeredis session.""" - if redis_session: - redis_loop.run_until_complete(redis_session.close()) - class StreamCommandTest(unittest.IsolatedAsyncioTestCase): @@ -30,44 +12,6 @@ class StreamCommandTest(unittest.IsolatedAsyncioTestCase): self.bot = MockBot() self.cog = Stream(self.bot) - def test_linking_time_format_from_alias_or_key(self): - """ - User provided time format needs to be lined to a proper entry in TIME_FORMATS - This Test checks _link_from_alias method - Checking for whether alias or key exists in TIME_FORMATS is done before calling this function - """ - - test_cases = (("sec", "second"), - ("s", "second"), - ("seconds", "second"), - ("second", "second"), - ("secs", "second"), - ("min", "minute"), - ("m", "minute"), - ("minutes", "minute"), - ("hr", "hour"), - ("hrs", "hour"), - ("hours", "hour"), - ("d", "day"), - ("days", "day"), - ("yr", "year"), - ("yrs", "year"), - ("y", "year")) - - for case in test_cases: - linked = self.cog._link_from_alias(case[0])[1] - self.assertEqual(linked, case[1]) - - def test_parsing_duration_and_time_format_to_seconds(self): - """ - Test calculating time in seconds from duration and time unit - This test is technically dependent on _link_from_alias function, not the best practice but necessary - """ - test_cases = ((1, "minute", 60), (5, "second", 5), (2, "day", 172800)) - for case in test_cases: - time_in_seconds = self.cog._parse_time_to_seconds(case[0], case[1]) - self.assertEqual(time_in_seconds, case[2]) - def test_checking_if_user_has_streaming_permission(self): """ Test searching for video role in Member.roles |