diff options
| author | 2020-11-27 08:44:20 +0100 | |
|---|---|---|
| committer | 2020-11-27 08:58:54 +0100 | |
| commit | 16936aad19978078a872ce8ebec82f30a3e7442f (patch) | |
| tree | c4a8b8e3bfad3944469316c859cc302b722c1479 | |
| parent | add success message after revokestream command (diff) | |
move to Scheduler
| -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 | 
