diff options
| -rw-r--r-- | Pipfile | 3 | ||||
| -rw-r--r-- | Pipfile.lock | 100 | ||||
| -rw-r--r-- | bot/api.py | 4 | ||||
| -rw-r--r-- | bot/cogs/moderation.py | 349 | ||||
| -rw-r--r-- | bot/converters.py | 21 | ||||
| -rw-r--r-- | bot/utils/moderation.py | 7 | ||||
| -rw-r--r-- | bot/utils/time.py | 2 | 
7 files changed, 291 insertions, 195 deletions
| @@ -19,6 +19,7 @@ aio-pika = "*"  python-dateutil = "*"  deepdiff = "*"  requests = "*" +dateparser = "*"  [dev-packages]  "flake8" = ">=3.6" @@ -32,7 +33,7 @@ dodgy = "*"  pytest = "*"  [requires] -python_version = "3.6" +python_version = "3.7"  [scripts]  start = "python -m bot" diff --git a/Pipfile.lock b/Pipfile.lock index e273e5276..6c565c85f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@  {      "_meta": {          "hash": { -            "sha256": "4f40c03d9fa30eb15e0bc6b962d42d3185e2b2cdf52d9b31969bbd119b9ed61e" +            "sha256": "52bb2561c3036c40f44d3c5da359a5089e7a543cb05d9c8525553207697c98a1"          },          "pipfile-spec": 6,          "requires": { @@ -147,6 +147,14 @@              ],              "version": "==3.0.4"          }, +        "dateparser": { +            "hashes": [ +                "sha256:940828183c937bcec530753211b70f673c0a9aab831e43273489b310538dff86", +                "sha256:b452ef8b36cd78ae86a50721794bc674aa3994e19b570f7ba92810f4e0a2ae03" +            ], +            "index": "pypi", +            "version": "==0.7.0" +        },          "deepdiff": {              "hashes": [                  "sha256:152b29dd9cd97cc78403121fb394925ec47377d4a410751e56547c3930ba2b39", @@ -232,39 +240,35 @@          },          "lxml": {              "hashes": [ -                "sha256:16cf8bac33ec17049617186d63006ba49da7c5be417042877a49f0ef6d7a195d", -                "sha256:18f2d8f14cc61e66e8a45f740d15b6fc683c096f733db1f8d0ee15bcac9843de", -                "sha256:260868f69d14a64dd1de9cf92e133d2f71514d288de4906f109bdf48ca9b756a", -                "sha256:29b8acd8ecdf772266dbac491f203c71664b0b07ad4309ba2c3bb131306332fc", -                "sha256:2b05e5e06f8e8c63595472dc887d0d6e0250af754a35ba690f6a6abf2ef85691", -                "sha256:30d6ec05fb607a5b7345549f642c7c7a5b747b634f6d5e935596b910f243f96f", -                "sha256:3bf683f0237449ebc1851098f664410e3c99ba3faa8c9cc82c6acfe857df1767", -                "sha256:3ce5488121eb15513c4b239dadd67f9e7959511bd766aac6be0c35e80274f298", -                "sha256:48be0c375350a5519bb9474b42a9c0e7ab709fb45f11bfcd33de876791137896", -                "sha256:49bc343ca3b30cd860845433bb9f62448a54ff87b632175108bacbc5dc63e49e", -                "sha256:4cc7531e86a43ea66601763c5914c3d3adb297f32e4284957609b90d41825fca", -                "sha256:4e9822fad564d82035f0b6d701a890444560210f8a8648b8f15850f8fe883cd9", -                "sha256:51a9a441aefc8c93512bad5efe867d2ff086e7249ce0fc3b47c310644b352936", -                "sha256:5bbed9efc8aeb69929140f71a30e655bf496b45b766861513960e1b11168d475", -                "sha256:60a5323b2bc893ca1059d283d6695a172d51cc95a70c25b3e587e1aad5459c38", -                "sha256:7035d9361f3ceec9ccc1dd3482094d1174580e7e1bf6870b77ea758f7cad15d2", -                "sha256:76d62cc048bda0ebf476689ad3eb8e65e6827e43a7521be3b163071020667b8c", -                "sha256:78163b578e6d1836012febaa1865e095ccc7fc826964dd69a2dbfe401618a1f7", -                "sha256:83b58b2b5904d50de03a47e2f56d24e9da4cf7e3b0d66fb4510b18fca0faf910", -                "sha256:a07447e46fffa5bb4d7a0af0a6505c8517e9bd197cfd2aec79e499b6e86cde49", -                "sha256:a17d808b3edca4aaf6b295b5a388c844a0b7f79aca2d79eec5acc1461db739e3", -                "sha256:a378fd61022cf4d3b492134c3bc48204ac2ff19e0813b23e07c3dd95ae8df0bc", -                "sha256:aa7d096a44ae3d475c5ed763e24cf302d32462e78b61bba73ce1ad0efb8f522a", -                "sha256:ade8785c93a985956ba6499d5ea6d0a362e24b4a9ba07dd18920fd67cccf63ea", -                "sha256:cc039668f91d8af8c4094cfb5a67c7ae733967fdc84c0507fe271db81480d367", -                "sha256:d89f1ffe98744c4b5c11f00fb843a4e72f68a6279b5e38168167f1b3c0fdd84c", -                "sha256:e691b6ef6e27437860016bd6c32e481bdc2ed3af03289707a38b9ca422105f40", -                "sha256:e750da6ac3ca624ae3303df448664012f9b6f9dfbc5d50048ea8a12ce2f8bc29", -                "sha256:eca305b200549906ea25648463aeb1b3b220b716415183eaa99c998a846936d9", -                "sha256:f52fe795e08858192eea167290033b5ff24f50f51781cb78d989e8d63cfe73d1" +                "sha256:0dd6589fa75d369ba06d2b5f38dae107f76ea127f212f6a7bee134f6df2d1d21", +                "sha256:1afbac344aa68c29e81ab56c1a9411c3663157b5aee5065b7fa030b398d4f7e0", +                "sha256:1baad9d073692421ad5dbbd81430aba6c7f5fdc347f03537ae046ddf2c9b2297", +                "sha256:1d8736421a2358becd3edf20260e41a06a0bf08a560480d3a5734a6bcbacf591", +                "sha256:1e1d9bddc5afaddf0de76246d3f2152f961697ad7439c559f179002682c45801", +                "sha256:1f179dc8b2643715f020f4d119d5529b02cd794c1c8f305868b73b8674d2a03f", +                "sha256:241fb7bdf97cb1df1edfa8f0bcdfd80525d4023dac4523a241907c8b2f44e541", +                "sha256:2f9765ee5acd3dbdcdc0d0c79309e01f7c16bc8d39b49250bf88de7b46daaf58", +                "sha256:312e1e1b1c3ce0c67e0b8105317323e12807955e8186872affb667dbd67971f6", +                "sha256:3273db1a8055ca70257fd3691c6d2c216544e1a70b673543e15cc077d8e9c730", +                "sha256:34dfaa8c02891f9a246b17a732ca3e99c5e42802416628e740a5d1cb2f50ff49", +                "sha256:3aa3f5288af349a0f3a96448ebf2e57e17332d99f4f30b02093b7948bd9f94cc", +                "sha256:51102e160b9d83c1cc435162d90b8e3c8c93b28d18d87b60c56522d332d26879", +                "sha256:56115fc2e2a4140e8994eb9585119a1ae9223b506826089a3ba753a62bd194a6", +                "sha256:69d83de14dbe8fe51dccfd36f88bf0b40f5debeac763edf9f8325180190eba6e", +                "sha256:99fdce94aeaa3ccbdfcb1e23b34273605c5853aa92ec23d84c84765178662c6c", +                "sha256:a7c0cd5b8a20f3093ee4a67374ccb3b8a126743b15a4d759e2a1bf098faac2b2", +                "sha256:abe12886554634ed95416a46701a917784cb2b4c77bfacac6916681d49bbf83d", +                "sha256:b4f67b5183bd5f9bafaeb76ad119e977ba570d2b0e61202f534ac9b5c33b4485", +                "sha256:bdd7c1658475cc1b867b36d5c4ed4bc316be8d3368abe03d348ba906a1f83b0e", +                "sha256:c6f24149a19f611a415a51b9bc5f17b6c2f698e0d6b41ffb3fa9f24d35d05d73", +                "sha256:d1e111b3ab98613115a208c1017f266478b0ab224a67bc8eac670fa0bad7d488", +                "sha256:d6520aa965773bbab6cb7a791d5895b00d02cf9adc93ac2bf4edb9ac1a6addc5", +                "sha256:dd185cde2ccad7b649593b0cda72021bc8a91667417001dbaf24cd746ecb7c11", +                "sha256:de2e5b0828a9d285f909b5d2e9d43f1cf6cf21fe65bc7660bdaa1780c7b58298", +                "sha256:f726444b8e909c4f41b4fde416e1071cf28fa84634bfb4befdf400933b6463af"              ],              "index": "pypi", -            "version": "==4.2.6" +            "version": "==4.3.0"          },          "markdownify": {              "hashes": [ @@ -506,6 +510,20 @@              "index": "pypi",              "version": "==3.13"          }, +        "regex": { +            "hashes": [ +                "sha256:15b4a185ae9782133f398f8ab7c29612a6e5f34ea9411e4cd36e91e78c347ebe", +                "sha256:3852b76f0b6d7bd98d328d548716c151b79017f2b81347360f26e5db10fb6503", +                "sha256:79a6a60ed1ee3b12eb0e828c01d75e3b743af6616d69add6c2fde1d425a4ba3f", +                "sha256:a2938c290b3be2c7cadafa21de3051f2ed23bfaf88728a1fe5dc552cbfdb0326", +                "sha256:aff7414712c9e6d260609da9c9af3aacebfbc307a4abe3376c7736e2a6c8563f", +                "sha256:d03782f0b0fa34f8f1dbdc94e27cf193b83c6105307a8c10563938c6d85180d9", +                "sha256:db79ac3d81e655dc12d38a865dd6d1b569a28fab4c53749051cd599a6eb7614f", +                "sha256:e803b3646c3f9c47f1f3dc870173c5d79c0fd2fd8e40bf917b97c7b56701baff", +                "sha256:e9660ccca360b6bd79606aab3672562ebb14bce6af6c501107364668543f4bef" +            ], +            "version": "==2018.11.22" +        },          "requests": {              "hashes": [                  "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", @@ -536,10 +554,10 @@          },          "soupsieve": {              "hashes": [ -                "sha256:057e08f362a255b457a5781675211556799ed3bb8807506eaac3809390bc304b", -                "sha256:f7d99b41637be2f249dfcc06ae93c13fcbbdfa7bb68b15308cdd0734e58146f1" +                "sha256:638535780f7b966411123d56eb3b89cd1d2e42d707270c6d7d053c7720a238f3", +                "sha256:cb61b59c55f9f6e91928a03fe4b500ac1fcef6f8e68082a630db098ab33e2126"              ], -            "version": "==1.6.1" +            "version": "==1.6.2"          },          "sphinx": {              "hashes": [ @@ -556,6 +574,12 @@              ],              "version": "==1.1.0"          }, +        "tzlocal": { +            "hashes": [ +                "sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e" +            ], +            "version": "==1.5.1" +        },          "urllib3": {              "hashes": [                  "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", @@ -769,11 +793,11 @@          },          "pytest": {              "hashes": [ -                "sha256:f689bf2fc18c4585403348dd56f47d87780bf217c53ed9ae7a3e2d7faa45f8e9", -                "sha256:f812ea39a0153566be53d88f8de94839db1e8a05352ed8a49525d7d7f37861e9" +                "sha256:3e65a22eb0d4f1bdbc1eacccf4a3198bf8d4049dea5112d70a0c61b00e748d02", +                "sha256:5924060b374f62608a078494b909d341720a050b5224ff87e17e12377486a71d"              ],              "index": "pypi", -            "version": "==4.0.2" +            "version": "==4.1.0"          },          "pyyaml": {              "hashes": [ diff --git a/bot/api.py b/bot/api.py index 0a2c192ce..2e1a239ba 100644 --- a/bot/api.py +++ b/bot/api.py @@ -29,6 +29,10 @@ class APIClient:          async with self.session.get(self._url_for(endpoint), *args, **kwargs) as resp:              return await resp.json() +    async def patch(self, endpoint: str, *args, **kwargs): +        async with self.session.patch(self._url_for(endpoint), *args, **kwargs) as resp: +            return await resp.json() +      async def post(self, endpoint: str, *args, **kwargs):          async with self.session.post(self._url_for(endpoint), *args, **kwargs) as resp:              return await resp.json() diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index d725755cd..51f4a3d79 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1,6 +1,7 @@  import asyncio  import logging  import textwrap +from datetime import datetime  from typing import Union  from discord import ( @@ -13,12 +14,12 @@ from discord.ext.commands import (  from bot import constants  from bot.cogs.modlog import ModLog  from bot.constants import Colours, Event, Icons, Keys, Roles, URLs -from bot.converters import InfractionSearchQuery +from bot.converters import ExpirationDate, InfractionSearchQuery  from bot.decorators import with_role  from bot.pagination import LinePaginator  from bot.utils.moderation import post_infraction  from bot.utils.scheduling import Scheduler, create_task -from bot.utils.time import parse_rfc1123, wait_until +from bot.utils.time import wait_until  log = logging.getLogger(__name__) @@ -59,16 +60,13 @@ class Moderation(Scheduler):      async def on_ready(self):          # Schedule expiration for previous infractions -        response = await self.bot.http_session.get( -            URLs.site_infractions, -            params={"dangling": "true"}, -            headers=self.headers +        infractions = await self.bot.api_client.get( +            'bot/infractions', params={'active': 'true'}          ) -        infraction_list = await response.json()          loop = asyncio.get_event_loop() -        for infraction_object in infraction_list: -            if infraction_object["expires_at"] is not None: -                self.schedule_task(loop, infraction_object["id"], infraction_object) +        for infraction in infractions: +            if infraction["expires_at"] is not None: +                self.schedule_task(loop, infraction["id"], infraction)      # region: Permanent infractions @@ -172,10 +170,23 @@ class Moderation(Scheduler):          :param reason: The reason for the ban.          """ +        active_bans = await self.bot.api_client.get( +            'bot/infractions', +            params={ +                'active': 'true', +                'type': 'ban', +                'user__id': str(user.id) +            } +        ) +        if active_bans: +            return await ctx.send( +                ":x: According to my records, this user is already banned. " +                f"See infraction **#{active_bans[0]['id']}**." +            ) +          notified = await self.notify_infraction(              user=user,              infr_type="Ban", -            duration="Permanent",              reason=reason          ) @@ -220,11 +231,23 @@ class Moderation(Scheduler):          :param reason: The reason for the mute.          """ +        active_mutes = await self.bot.api_client.get( +            'bot/infractions', +            params={ +                'active': 'true', +                'type': 'mute', +                'user__id': str(user.id) +            } +        ) +        if active_mutes: +            return await ctx.send( +                ":x: According to my records, this user is already muted. " +                f"See infraction **#{active_mutes[0]['id']}**." +            ) +          notified = await self.notify_infraction( -            user=user, -            infr_type="Mute", -            duration="Permanent", -            reason=reason +            user=user, infr_type="Mute", +            expires_at="Permanent", reason=reason          )          response_object = await post_infraction(ctx, user, type="mute", reason=reason) @@ -264,7 +287,10 @@ class Moderation(Scheduler):      @with_role(*MODERATION_ROLES)      @command(name="tempmute") -    async def tempmute(self, ctx: Context, user: Member, duration: str, *, reason: str = None): +    async def tempmute( +            self, ctx: Context, user: Member, expiration: ExpirationDate, +            *, reason: str = None +    ):          """          Create a temporary mute infraction in the database for a user.          :param user: Accepts user mention, ID, etc. @@ -272,25 +298,42 @@ class Moderation(Scheduler):          :param reason: The reason for the temporary mute.          """ +        active_mutes = await self.bot.api_client.get( +            'bot/infractions', +            params={ +                'active': 'true', +                'type': 'mute', +                'user__id': str(user.id) +            } +        ) +        if active_mutes: +            return await ctx.send( +                ":x: According to my records, this user is already muted. " +                f"See infraction **#{active_mutes[0]['id']}**." +            ) +          notified = await self.notify_infraction( -            user=user, -            infr_type="Mute", -            duration=duration, -            reason=reason +            user=user, infr_type="Mute", +            expires_at=expiration, reason=reason          ) -        response_object = await post_infraction(ctx, user, type="mute", reason=reason, duration=duration) -        if response_object is None: -            return +        infraction = await post_infraction( +            ctx, user, +            type="mute", reason=reason, +            expires_at=expiration +        )          self.mod_log.ignore(Event.member_update, user.id)          await user.add_roles(self._muted_role, reason=reason) -        infraction_object = response_object["infraction"] -        infraction_expiration = infraction_object["expires_at"] +        infraction_expiration = ( +            datetime +            .fromisoformat(infraction["expires_at"][:-1]) +            .strftime('%c') +        )          loop = asyncio.get_event_loop() -        self.schedule_task(loop, infraction_object["id"], infraction_object) +        self.schedule_task(loop, infraction["id"], infraction)          dm_result = ":incoming_envelope: " if notified else ""          action = f"{dm_result}:ok_hand: muted {user.mention} until {infraction_expiration}" @@ -313,30 +356,47 @@ class Moderation(Scheduler):                  Member: {user.mention} (`{user.id}`)                  Actor: {ctx.message.author}                  Reason: {reason} -                Duration: {duration}                  Expires: {infraction_expiration}              """)          )      @with_role(*MODERATION_ROLES)      @command(name="tempban") -    async def tempban(self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None): +    async def tempban( +            self, ctx: Context, user: Union[User, proxy_user], expiry: ExpirationDate, +            *, reason: str = None +    ):          """          Create a temporary ban infraction in the database for a user.          :param user: Accepts user mention, ID, etc. -        :param duration: The duration for the temporary ban infraction +        :param expiry: The duration for the temporary ban infraction          :param reason: The reason for the temporary ban.          """ +        active_bans = await self.bot.api_client.get( +            'bot/infractions', +            params={ +                'active': 'true', +                'type': 'ban', +                'user__id': str(user.id) +            } +        ) +        if active_bans: +            return await ctx.send( +                ":x: According to my records, this user is already banned. " +                f"See infraction **#{active_bans[0]['id']}**." +            ) +          notified = await self.notify_infraction( -            user=user, -            infr_type="Ban", -            duration=duration, -            reason=reason +            user=user, infr_type="Ban", +            expires_at=expiry, reason=reason          ) -        response_object = await post_infraction(ctx, user, type="ban", reason=reason, duration=duration) -        if response_object is None: +        infraction = await post_infraction( +            ctx, user, type="ban", +            reason=reason, expires_at=expiry +        ) +        if infraction is None:              return          self.mod_log.ignore(Event.member_ban, user.id) @@ -344,11 +404,14 @@ class Moderation(Scheduler):          guild: Guild = ctx.guild          await guild.ban(user, reason=reason, delete_message_days=0) -        infraction_object = response_object["infraction"] -        infraction_expiration = infraction_object["expires_at"] +        infraction_expiration = ( +            datetime +            .fromisoformat(infraction["expires_at"][:-1]) +            .strftime('%c') +        )          loop = asyncio.get_event_loop() -        self.schedule_task(loop, infraction_object["id"], infraction_object) +        self.schedule_task(loop, infraction["id"], infraction)          dm_result = ":incoming_envelope: " if notified else ""          action = f"{dm_result}:ok_hand: banned {user.mention} until {infraction_expiration}" @@ -371,7 +434,6 @@ class Moderation(Scheduler):                  Member: {user.mention} (`{user.id}`)                  Actor: {ctx.message.author}                  Reason: {reason} -                Duration: {duration}                  Expires: {infraction_expiration}              """)          ) @@ -635,27 +697,26 @@ class Moderation(Scheduler):          try:              # check the current active infraction -            response = await self.bot.http_session.get( -                URLs.site_infractions_user_type_current.format( -                    user_id=user.id, -                    infraction_type="mute" -                ), -                headers=self.headers +            response = await self.bot.api_client.get( +                'bot/infractions', +                params={ +                    'active': 'true', +                    'type': 'mute', +                    'user__id': user.id +                }              ) -            response_object = await response.json() -            if "error_code" in response_object: -                await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}") -                return +            if len(response) > 1: +                log.warning("Found more than one active mute infraction for user `%d`", user.id) -            infraction_object = response_object["infraction"] -            if infraction_object is None: +            if not response:                  # no active infraction                  await ctx.send(f":x: There is no active mute infraction for user {user.mention}.")                  return -            await self._deactivate_infraction(infraction_object) -            if infraction_object["expires_at"] is not None: -                self.cancel_expiration(infraction_object["id"]) +            infraction = response[0] +            await self._deactivate_infraction(infraction) +            if infraction["expires_at"] is not None: +                self.cancel_expiration(infraction["id"])              notified = await self.notify_pardon(                  user=user, @@ -679,7 +740,7 @@ class Moderation(Scheduler):                  text=textwrap.dedent(f"""                      Member: {user.mention} (`{user.id}`)                      Actor: {ctx.message.author} -                    Intended expiry: {infraction_object['expires_at']} +                    Intended expiry: {infraction['expires_at']}                  """)              )          except Exception: @@ -697,27 +758,29 @@ class Moderation(Scheduler):          try:              # check the current active infraction -            response = await self.bot.http_session.get( -                URLs.site_infractions_user_type_current.format( -                    user_id=user.id, -                    infraction_type="ban" -                ), -                headers=self.headers +            response = await self.bot.api_client.get( +                'bot/infractions', +                params={ +                    'active': 'true', +                    'type': 'ban', +                    'user__id': str(user.id) +                }              ) -            response_object = await response.json() -            if "error_code" in response_object: -                await ctx.send(f":x: There was an error removing the infraction: {response_object['error_message']}") -                return +            if len(response) > 1: +                log.warning( +                    "More than one active ban infraction found for user `%d`.", +                    user.id +                ) -            infraction_object = response_object["infraction"] -            if infraction_object is None: +            if not response:                  # no active infraction                  await ctx.send(f":x: There is no active ban infraction for user {user.mention}.")                  return -            await self._deactivate_infraction(infraction_object) -            if infraction_object["expires_at"] is not None: -                self.cancel_expiration(infraction_object["id"]) +            infraction = response[0] +            await self._deactivate_infraction(infraction) +            if infraction["expires_at"] is not None: +                self.cancel_expiration(infraction["id"])              await ctx.send(f":ok_hand: Un-banned {user.mention}.") @@ -730,7 +793,7 @@ class Moderation(Scheduler):                  text=textwrap.dedent(f"""                      Member: {user.mention} (`{user.id}`)                      Actor: {ctx.message.author} -                    Intended expiry: {infraction_object['expires_at']} +                    Intended expiry: {infraction['expires_at']}                  """)              )          except Exception: @@ -757,60 +820,64 @@ class Moderation(Scheduler):      @with_role(*MODERATION_ROLES)      @infraction_edit_group.command(name="duration") -    async def edit_duration(self, ctx: Context, infraction_id: str, duration: str): +    async def edit_duration( +            self, ctx: Context, +            infraction_id: int, expires_at: Union[ExpirationDate, str] +    ):          """          Sets the duration of the given infraction, relative to the time of updating. -        :param infraction_id: the id (UUID) of the infraction -        :param duration: the new duration of the infraction, relative to the time of updating. Use "permanent" to mark -        the infraction as permanent. +        :param infraction_id: the id of the infraction +        :param expires_at: the new expiration date of the infraction. +        Use "permanent" to mark the infraction as permanent.          """ -        try: -            previous = await self.bot.http_session.get( -                URLs.site_infractions_by_id.format( -                    infraction_id=infraction_id -                ), -                headers=self.headers +        if isinstance(expires_at, str) and expires_at != 'permanent': +            raise BadArgument( +                "If `expires_at` is given as a non-datetime, " +                "it must be `permanent`."              ) +        if expires_at == 'permanent': +            expires_at = None -            previous_object = await previous.json() +        try: +            previous_infraction = await self.bot.api_client.get( +                'bot/infractions/' + str(infraction_id) +            ) -            if duration == "permanent": -                duration = None              # check the current active infraction -            response = await self.bot.http_session.patch( -                URLs.site_infractions, +            infraction = await self.bot.api_client.patch( +                'bot/infractions/' + str(infraction_id),                  json={ -                    "id": infraction_id, -                    "duration": duration -                }, -                headers=self.headers +                    'expires_at': ( +                        expires_at.isoformat() +                        if expires_at is not None +                        else None +                    ) +                }              ) -            response_object = await response.json() -            if "error_code" in response_object or response_object.get("success") is False: -                await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}") -                return -            infraction_object = response_object["infraction"]              # Re-schedule -            self.cancel_task(infraction_id) +            self.cancel_task(infraction['id'])              loop = asyncio.get_event_loop() -            self.schedule_task(loop, infraction_object["id"], infraction_object) +            self.schedule_task(loop, infraction['id'], infraction) -            if duration is None: +            if expires_at is None:                  await ctx.send(f":ok_hand: Updated infraction: marked as permanent.")              else: -                await ctx.send(f":ok_hand: Updated infraction: set to expire on {infraction_object['expires_at']}.") +                human_expiry = ( +                    datetime +                    .fromisoformat(infraction['expires_at'][:-1]) +                    .strftime('%c') +                ) +                await ctx.send(f":ok_hand: Updated infraction: set to expire on {human_expiry}.")          except Exception:              log.exception("There was an error updating an infraction.")              await ctx.send(":x: There was an error updating the infraction.")              return -        prev_infraction = previous_object["infraction"] -          # Get information about the infraction's user -        user_id = int(infraction_object["user"]["user_id"]) +        user_id = infraction["user"]          user = ctx.guild.get_member(user_id)          if user: @@ -821,7 +888,7 @@ class Moderation(Scheduler):              thumbnail = None          # The infraction's actor -        actor_id = int(infraction_object["actor"]["user_id"]) +        actor_id = infraction["actor"]          actor = ctx.guild.get_member(actor_id) or f"`{actor_id}`"          await self.mod_log.send_log_message( @@ -833,54 +900,38 @@ class Moderation(Scheduler):                  Member: {member_text}                  Actor: {actor}                  Edited by: {ctx.message.author} -                Previous expiry: {prev_infraction['expires_at']} -                New expiry: {infraction_object['expires_at']} +                Previous expiry: {previous_infraction['expires_at']} +                New expiry: {infraction['expires_at']}              """)          )      @with_role(*MODERATION_ROLES)      @infraction_edit_group.command(name="reason") -    async def edit_reason(self, ctx: Context, infraction_id: str, *, reason: str): +    async def edit_reason(self, ctx: Context, infraction_id: int, *, reason: str):          """          Sets the reason of the given infraction. -        :param infraction_id: the id (UUID) of the infraction +        :param infraction_id: the id of the infraction          :param reason: The new reason of the infraction          """          try: -            previous = await self.bot.http_session.get( -                URLs.site_infractions_by_id.format( -                    infraction_id=infraction_id -                ), -                headers=self.headers +            old_infraction = await self.bot.api_client.get( +                'bot/infractions/' + str(infraction_id)              ) -            previous_object = await previous.json() - -            response = await self.bot.http_session.patch( -                URLs.site_infractions, -                json={ -                    "id": infraction_id, -                    "reason": reason -                }, -                headers=self.headers +            updated_infraction = await self.bot.api_client.patch( +                'bot/infractions/' + str(infraction_id), +                json={'reason': reason}              ) -            response_object = await response.json() -            if "error_code" in response_object or response_object.get("success") is False: -                await ctx.send(f":x: There was an error updating the infraction: {response_object['error_message']}") -                return -              await ctx.send(f":ok_hand: Updated infraction: set reason to \"{reason}\".") +          except Exception:              log.exception("There was an error updating an infraction.")              await ctx.send(":x: There was an error updating the infraction.")              return -        new_infraction = response_object["infraction"] -        prev_infraction = previous_object["infraction"] -          # Get information about the infraction's user -        user_id = int(new_infraction["user"]["user_id"]) +        user_id = updated_infraction['user']          user = ctx.guild.get_member(user_id)          if user: @@ -891,7 +942,7 @@ class Moderation(Scheduler):              thumbnail = None          # The infraction's actor -        actor_id = int(new_infraction["actor"]["user_id"]) +        actor_id = updated_infraction['actor']          actor = ctx.guild.get_member(actor_id) or f"`{actor_id}`"          await self.mod_log.send_log_message( @@ -903,8 +954,8 @@ class Moderation(Scheduler):                  Member: {user_text}                  Actor: {actor}                  Edited by: {ctx.message.author} -                Previous reason: {prev_infraction['reason']} -                New reason: {new_infraction['reason']} +                Previous reason: {old_infraction['reason']} +                New reason: {updated_infraction['reason']}              """)          ) @@ -1023,7 +1074,7 @@ class Moderation(Scheduler):          infraction_id = infraction_object["id"]          # transform expiration to delay in seconds -        expiration_datetime = parse_rfc1123(infraction_object["expires_at"]) +        expiration_datetime = datetime.fromisoformat(infraction_object["expires_at"][:-1])          await wait_until(expiration_datetime)          log.debug(f"Marking infraction {infraction_id} as inactive (expired).") @@ -1032,7 +1083,7 @@ class Moderation(Scheduler):          self.cancel_task(infraction_object["id"])          # Notify the user that they've been unmuted. -        user_id = int(infraction_object["user"]["user_id"]) +        user_id = infraction_object["user"]          guild = self.bot.get_guild(constants.Guild.id)          await self.notify_pardon(              user=guild.get_member(user_id), @@ -1043,13 +1094,13 @@ class Moderation(Scheduler):      async def _deactivate_infraction(self, infraction_object):          """ -        A co-routine which marks an infraction as inactive on the website. This co-routine does not cancel or -        un-schedule an expiration task. +        A co-routine which marks an infraction as inactive on the website. +        This co-routine does not cancel or un-schedule an expiration task.          :param infraction_object: the infraction in question          """          guild: Guild = self.bot.get_guild(constants.Guild.id) -        user_id = int(infraction_object["user"]["user_id"]) +        user_id = infraction_object["user"]          infraction_type = infraction_object["type"]          if infraction_type == "mute": @@ -1064,13 +1115,9 @@ class Moderation(Scheduler):              user: Object = Object(user_id)              await guild.unban(user) -        await self.bot.http_session.patch( -            URLs.site_infractions, -            headers=self.headers, -            json={ -                "id": infraction_object["id"], -                "active": False -            } +        await self.bot.api_client.patch( +            'bot/infractions/' + str(infraction_object['id']), +            json={"active": False}          )      def _infraction_to_string(self, infraction_object): @@ -1098,7 +1145,8 @@ class Moderation(Scheduler):          return lines.strip()      async def notify_infraction( -            self, user: Union[User, Member], infr_type: str, duration: str = None, reason: str = None +            self, user: Union[User, Member], infr_type: str, +            expires_at: Union[datetime, str] = 'N/A', reason: str = "No reason provided."      ):          """          Notify a user of their fresh infraction :) @@ -1109,16 +1157,13 @@ class Moderation(Scheduler):          :param reason: The reason for the infraction.          """ -        if duration is None: -            duration = "N/A" - -        if reason is None: -            reason = "No reason provided." +        if isinstance(expires_at, datetime): +            expires_at = expires_at.strftime('%c')          embed = Embed(              description=textwrap.dedent(f"""                  **Type:** {infr_type} -                **Duration:** {duration} +                **Expires:** {expires_at}                  **Reason:** {reason}                  """),              colour=Colour(Colours.soft_red) diff --git a/bot/converters.py b/bot/converters.py index 069e841f9..1100b502c 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -1,8 +1,10 @@  import logging  import random  import socket +from datetime import datetime  from ssl import CertificateError +import dateparser  import discord  from aiohttp import AsyncResolver, ClientConnectorError, ClientSession, TCPConnector  from discord.ext.commands import BadArgument, Context, Converter @@ -254,3 +256,22 @@ class TagContentConverter(Converter):              raise BadArgument("Tag contents should not be empty, or filled with whitespace.")          return tag_content + + +class ExpirationDate(Converter): +    DATEPARSER_SETTINGS = { +        'PREFER_DATES_FROM': 'future', +        'TIMEZONE': 'UTC', +        'TO_TIMEZONE': 'UTC' +    } + +    async def convert(self, ctx, expiration_string: str): +        expiry = dateparser.parse(expiration_string, settings=self.DATEPARSER_SETTINGS) +        if expiry is None: +            raise BadArgument(f"Failed to parse expiration date from `{expiration_string}`") + +        now = datetime.utcnow() +        if expiry < now: +            expiry = now + (now - expiry) + +        return expiry diff --git a/bot/utils/moderation.py b/bot/utils/moderation.py index 459fe6eb3..2611ee993 100644 --- a/bot/utils/moderation.py +++ b/bot/utils/moderation.py @@ -1,4 +1,5 @@  import logging +from datetime import datetime  from typing import Union  from aiohttp import ClientError @@ -14,7 +15,7 @@ HEADERS = {"X-API-KEY": Keys.site_api}  async def post_infraction(      ctx: Context, user: Union[Member, Object, User], -    type: str, reason: str, duration: str = None, hidden: bool = False +    type: str, reason: str, expires_at: datetime = None, hidden: bool = False  ):      payload = { @@ -24,8 +25,8 @@ async def post_infraction(          "type": type,          "user": user.id      } -    if duration: -        payload['duration'] = duration +    if expires_at: +        payload['expires_at'] = expires_at.isoformat()      try:          response = await ctx.bot.api_client.post( diff --git a/bot/utils/time.py b/bot/utils/time.py index 8e5d4e1bd..a330c9cd8 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -106,7 +106,7 @@ async def wait_until(time: datetime.datetime):      :param time: A datetime.datetime object to wait until.      """ -    delay = time - datetime.datetime.now(tz=datetime.timezone.utc) +    delay = time - datetime.datetime.utcnow()      delay_seconds = delay.total_seconds()      if delay_seconds > 1.0: | 
