diff options
author | 2019-01-06 20:18:51 +0100 | |
---|---|---|
committer | 2019-01-06 20:19:06 +0100 | |
commit | 6e640122cca34bd8a060f75f44a878a00f2b30c5 (patch) | |
tree | 36dcef814729ab18574d6092060d5b68fe4e4192 | |
parent | Move note and warning creation to Django API backend. (diff) |
Move all moderation commands to the Django API.
-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: |