From 47ed2339e55eb2a0bc245b45c1f0df9cc8b9af36 Mon Sep 17 00:00:00 2001 From: swfarnsworth Date: Thu, 25 Feb 2021 00:53:12 -0500 Subject: Instructions to dispute an infraction vary by infraction type. Previously, the user was instructed to email the appeals email for infraction types that don't remove one from the server. They are now instructed to DM ModMail except for Ban-type infractions. Also removed the URL string literal from the hyperlink to that URL. --- bot/exts/moderation/infraction/_utils.py | 44 +++++++++++----------- tests/bot/exts/moderation/infraction/test_utils.py | 6 +-- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index e766c1e5c..e58c2b22f 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -22,7 +22,6 @@ INFRACTION_ICONS = { "voice_ban": (Icons.voice_state_red, Icons.voice_state_green), } RULES_URL = "https://pythondiscord.com/pages/rules" -APPEALABLE_INFRACTIONS = ("ban", "mute", "voice_ban") # Type aliases UserObject = t.Union[discord.Member, discord.User] @@ -31,8 +30,10 @@ Infraction = t.Dict[str, t.Union[str, int, bool]] APPEAL_EMAIL = "appeals@pythondiscord.com" -INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}" -INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" +INFRACTION_TITLE = "Please review our rules" +INFRACTION_APPEAL_EMAIL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" +INFRACTION_APPEAL_MODMAIL_FOOTER = ('If you would like to discuss or appeal this infraction, ' + 'send a message to the ModMail bot') INFRACTION_AUTHOR_NAME = "Infraction information" INFRACTION_DESCRIPTION_TEMPLATE = ( @@ -71,13 +72,13 @@ async def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]: async def post_infraction( - ctx: Context, - user: UserSnowflake, - infr_type: str, - reason: str, - expires_at: datetime = None, - hidden: bool = False, - active: bool = True + ctx: Context, + user: UserSnowflake, + infr_type: str, + reason: str, + expires_at: datetime = None, + hidden: bool = False, + active: bool = True ) -> t.Optional[dict]: """Posts an infraction to the API.""" if isinstance(user, (discord.Member, discord.User)) and user.bot: @@ -150,11 +151,11 @@ async def get_active_infraction( async def notify_infraction( - user: UserObject, - infr_type: str, - expires_at: t.Optional[str] = None, - reason: t.Optional[str] = None, - icon_url: str = Icons.token_removed + user: UserObject, + infr_type: str, + expires_at: t.Optional[str] = None, + reason: t.Optional[str] = None, + icon_url: str = Icons.token_removed ) -> bool: """DM a user about their new infraction and return True if the DM is successful.""" log.trace(f"Sending {user} a DM about their {infr_type} infraction.") @@ -178,17 +179,18 @@ async def notify_infraction( embed.title = INFRACTION_TITLE embed.url = RULES_URL - if infr_type in APPEALABLE_INFRACTIONS: - embed.set_footer(text=INFRACTION_APPEAL_FOOTER) + embed.set_footer( + text=INFRACTION_APPEAL_EMAIL_FOOTER if infr_type == 'Ban' else INFRACTION_APPEAL_MODMAIL_FOOTER + ) return await send_private_embed(user, embed) async def notify_pardon( - user: UserObject, - title: str, - content: str, - icon_url: str = Icons.user_verified + user: UserObject, + title: str, + content: str, + icon_url: str = Icons.user_verified ) -> bool: """DM a user about their pardoned infraction and return True if the DM is successful.""" log.trace(f"Sending {user} a DM about their pardoned infraction.") diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 5b62463e0..ef6127344 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -146,7 +146,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.token_removed - ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), + ).set_footer(text=utils.INFRACTION_APPEAL_EMAIL_FOOTER), "send_result": True }, { @@ -200,7 +200,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.defcon_denied - ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), + ).set_footer(text=utils.INFRACTION_APPEAL_EMAIL_FOOTER), "send_result": False }, { @@ -218,7 +218,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.defcon_denied - ).set_footer(text=utils.INFRACTION_APPEAL_FOOTER), + ).set_footer(text=utils.INFRACTION_APPEAL_EMAIL_FOOTER), "send_result": True } ] -- cgit v1.2.3 From 75f2b9d5e922db8aca2c873c214455fded02fc4d Mon Sep 17 00:00:00 2001 From: swfarnsworth Date: Sun, 28 Feb 2021 11:33:26 -0500 Subject: Update the tests to reflect changes in expected behavior. The DM sent to infracted users now instructs them to DM modmail if they want to discuss non-ban infractions, so the tests now check if that instruction is present. Note that there already exists a superfluous test for note infractions, for which no DM is sent by design. --- tests/bot/exts/moderation/infraction/test_utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index ef6127344..ee9ff650c 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -146,7 +146,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.token_removed - ).set_footer(text=utils.INFRACTION_APPEAL_EMAIL_FOOTER), + ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), "send_result": True }, { @@ -164,9 +164,11 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.token_removed - ), + ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), "send_result": False }, + # Note that this test case asserts that the DM that *would* get sent to the user is formatted + # correctly, even though that message is deliberately never sent. { "args": (self.user, "note", None, None, Icons.defcon_denied), "expected_output": Embed( @@ -182,7 +184,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.defcon_denied - ), + ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), "send_result": False }, { @@ -200,7 +202,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.defcon_denied - ).set_footer(text=utils.INFRACTION_APPEAL_EMAIL_FOOTER), + ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), "send_result": False }, { @@ -218,7 +220,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): name=utils.INFRACTION_AUTHOR_NAME, url=utils.RULES_URL, icon_url=Icons.defcon_denied - ).set_footer(text=utils.INFRACTION_APPEAL_EMAIL_FOOTER), + ).set_footer(text=utils.INFRACTION_APPEAL_MODMAIL_FOOTER), "send_result": True } ] -- cgit v1.2.3 From 54952d11339ce4e065f061064098e76a780d5644 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 3 Mar 2021 15:58:50 +0200 Subject: Add disable_header to watchchannel to disable talentpool headers We need to disable this, because new format of nominations don't match with it. --- bot/exts/moderation/watchchannels/_watchchannel.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index f9fc12dc3..0793a66af 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -47,7 +47,9 @@ class WatchChannel(metaclass=CogABCMeta): webhook_id: int, api_endpoint: str, api_default_params: dict, - logger: logging.Logger + logger: logging.Logger, + *, + disable_header: bool = False ) -> None: self.bot = bot @@ -66,6 +68,7 @@ class WatchChannel(metaclass=CogABCMeta): self.channel = None self.webhook = None self.message_history = MessageHistory() + self.disable_header = disable_header self._start = self.bot.loop.create_task(self.start_watchchannel()) @@ -267,6 +270,9 @@ class WatchChannel(metaclass=CogABCMeta): async def send_header(self, msg: Message) -> None: """Sends a header embed with information about the relayed messages to the watch channel.""" + if self.disable_header: + return + user_id = msg.author.id guild = self.bot.get_guild(GuildConfig.id) -- cgit v1.2.3 From 86988b6bde5fcbbdf2445a5a4f2f1df68d5a7754 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 3 Mar 2021 16:11:09 +0200 Subject: Migrate talentpool to new schema - Add disable_header to watchchannel initialization. We don't have root actor field anymore, so headers give error and there is no point to rewrite this, because this will be removed soon. - Removed duplicates check of nominations of one user. Now as API allows this, multiple actors can nomination one user. - Add special error message if same actor have already nominated user Every actor can only have 1 nomination entry. - Remove previous reason from watch command We don't store reason that way anymore, and we don't want that this message spam whole channel. - Split end reason and reason editing commands. API PATCH request buildup have been changed, so changing both of them in one command don't make sense anymore. - Migrate nomination string generation --- bot/exts/moderation/watchchannels/talentpool.py | 86 +++++++++++++++++-------- 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index dd3349c3a..1649d4d48 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -28,6 +28,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): api_endpoint='bot/nominations', api_default_params={'active': 'true', 'ordering': '-inserted_at'}, logger=log, + disable_header=True, ) @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) @@ -83,10 +84,6 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await ctx.send(f":x: Failed to update the user cache; can't add {user}") return - if user.id in self.watched_users: - await ctx.send(f":x: {user} is already being watched in the talent pool") - return - # Manual request with `raise_for_status` as False because we want the actual response session = self.bot.api_client.session url = self.bot.api_client._url_for(self.api_endpoint) @@ -101,8 +98,12 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): async with session.post(url, **kwargs) as resp: response_data = await resp.json() - if resp.status == 400 and response_data.get('user', False): - await ctx.send(":x: The specified user can't be found in the database tables") + if resp.status == 400: + if response_data.get('user', False): + await ctx.send(":x: The specified user can't be found in the database tables") + elif response_data.get('actor', False): + await ctx.send(":x: You already have nominated this user") + return else: resp.raise_for_status() @@ -120,9 +121,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) if history: - total = f"({len(history)} previous nominations in total)" - start_reason = f"Watched: {textwrap.shorten(history[0]['reason'], width=500, placeholder='...')}" - msg += f"\n\nUser's previous watch reasons {total}:```{start_reason}```" + msg += f"\n\n{len(history)} previous nominations in total" await ctx.send(msg) @@ -176,13 +175,39 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): @nomination_edit_group.command(name='reason') @has_any_role(*MODERATION_ROLES) - async def edit_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: - """ - Edits the reason/unnominate reason for the nomination with the given `id` depending on the status. + async def edit_reason_command(self, ctx: Context, nomination_id: int, actor: FetchedMember, *, reason: str) -> None: + """Edits the reason of `actor` entry for the nomination with the given `id`.""" + try: + nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") + except ResponseCodeError as e: + if e.response.status == 404: + self.log.trace(f"Nomination API 404: Can't nomination with id {nomination_id}") + await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") + return + else: + raise - If the nomination is active, the reason for nominating the user will be edited; - If the nomination is no longer active, the reason for ending the nomination will be edited instead. - """ + if not nomination["active"]: + await ctx.send(":x: Can't edit reason of ended nomination.") + return + + if not any(entry["actor"] == actor.id for entry in nomination["entries"]): + await ctx.send(f":x: {actor} don't have entry for this nomination.") + return + + self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {reason}") + + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination_id}", + json={"actor": actor.id, "reason": reason} + ) + await self.fetch_user_cache() # Update cache + await ctx.send(":white_check_mark: Successfully updates reason of nomination.") + + @nomination_edit_group.command(name='end_reason') + @has_any_role(*MODERATION_ROLES) + async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: + """Edits the unnominate reason for the nomination with the given `id`.""" try: nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") except ResponseCodeError as e: @@ -193,16 +218,18 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): else: raise - field = "reason" if nomination["active"] else "end_reason" + if nomination["active"]: + await ctx.send(":x: Cannot edit end reason of active nomination.") + return - self.log.trace(f"Changing {field} for nomination with id {nomination_id} to {reason}") + self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {reason}") await self.bot.api_client.patch( f"{self.api_endpoint}/{nomination_id}", - json={field: reason} + json={"end_reason": reason} ) await self.fetch_user_cache() # Update cache. - await ctx.send(f":white_check_mark: Updated the {field} of the nomination!") + await ctx.send(":white_check_mark: Updated the end reason of the nomination!") @Cog.listener() async def on_member_ban(self, guild: Guild, user: Union[User, Member]) -> None: @@ -237,13 +264,18 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): def _nomination_to_string(self, nomination_object: dict) -> str: """Creates a string representation of a nomination.""" guild = self.bot.get_guild(Guild.id) + entries = [] + for site_entry in nomination_object["entries"]: + actor_id = site_entry["actor"] + actor = guild.get_member(actor_id) - actor_id = nomination_object["actor"] - actor = guild.get_member(actor_id) + reason = site_entry["reason"] or "*None*" + created = time.format_infraction(site_entry["inserted_at"]) + entries.append(f"Actor: {actor or actor_id}\nReason: {reason}\nCreated: {created}") - active = nomination_object["active"] + entries_string = "\n\n".join(entries) - reason = nomination_object["reason"] or "*None*" + active = nomination_object["active"] start_date = time.format_infraction(nomination_object["inserted_at"]) if active: @@ -252,9 +284,9 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): =============== Status: **Active** Date: {start_date} - Actor: {actor.mention if actor else actor_id} - Reason: {reason} Nomination ID: `{nomination_object["id"]}` + + {entries_string} =============== """ ) @@ -265,8 +297,8 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): =============== Status: Inactive Date: {start_date} - Actor: {actor.mention if actor else actor_id} - Reason: {reason} + + {entries_string} End date: {end_date} Unwatch reason: {nomination_object["end_reason"]} -- cgit v1.2.3 From b9141ea4def9868fb0f17476bcd4e4a6742c0afd Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:15:11 +0200 Subject: Add parentheses back to previous nominations count Co-authored-by: Boris Muratov <8bee278@gmail.com> --- bot/exts/moderation/watchchannels/talentpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index 1649d4d48..11c629f1e 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -121,7 +121,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) if history: - msg += f"\n\n{len(history)} previous nominations in total" + msg += f"\n\n({len(history)} previous nominations in total)" await ctx.send(msg) -- cgit v1.2.3 From 4532b405a590c5a45cffb90d48ff238f1e1cf7d4 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:32:31 +0200 Subject: Fix trace logging of nomination 404 --- bot/exts/moderation/watchchannels/talentpool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index 11c629f1e..e5414b0c9 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -181,7 +181,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") except ResponseCodeError as e: if e.response.status == 404: - self.log.trace(f"Nomination API 404: Can't nomination with id {nomination_id}") + self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") return else: @@ -212,7 +212,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") except ResponseCodeError as e: if e.response.status == 404: - self.log.trace(f"Nomination API 404: Can't nomination with id {nomination_id}") + self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") return else: -- cgit v1.2.3 From ce5fb702639fa013c608f5e53059722aee68f6b8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:35:19 +0200 Subject: Fix grammar of nomination cog Co-authored-by: Boris Muratov <8bee278@gmail.com> --- bot/exts/moderation/watchchannels/talentpool.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index e5414b0c9..938720cc0 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -102,7 +102,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): if response_data.get('user', False): await ctx.send(":x: The specified user can't be found in the database tables") elif response_data.get('actor', False): - await ctx.send(":x: You already have nominated this user") + await ctx.send(":x: You have already nominated this user") return else: @@ -176,7 +176,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): @nomination_edit_group.command(name='reason') @has_any_role(*MODERATION_ROLES) async def edit_reason_command(self, ctx: Context, nomination_id: int, actor: FetchedMember, *, reason: str) -> None: - """Edits the reason of `actor` entry for the nomination with the given `id`.""" + """Edits the reason of a specific nominator in a specific active nomination.""" try: nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") except ResponseCodeError as e: @@ -188,21 +188,21 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): raise if not nomination["active"]: - await ctx.send(":x: Can't edit reason of ended nomination.") + await ctx.send(":x: Can't edit the reason of an inactive nomination.") return if not any(entry["actor"] == actor.id for entry in nomination["entries"]): - await ctx.send(f":x: {actor} don't have entry for this nomination.") + await ctx.send(f":x: {actor} doesn't have an entry in this nomination.") return - self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {reason}") + self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}") await self.bot.api_client.patch( f"{self.api_endpoint}/{nomination_id}", json={"actor": actor.id, "reason": reason} ) await self.fetch_user_cache() # Update cache - await ctx.send(":white_check_mark: Successfully updates reason of nomination.") + await ctx.send(":white_check_mark: Successfully updated nomination reason.") @nomination_edit_group.command(name='end_reason') @has_any_role(*MODERATION_ROLES) @@ -219,10 +219,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): raise if nomination["active"]: - await ctx.send(":x: Cannot edit end reason of active nomination.") + await ctx.send(":x: Can't edit the end reason of an active nomination.") return - self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {reason}") + self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}") await self.bot.api_client.patch( f"{self.api_endpoint}/{nomination_id}", -- cgit v1.2.3 From 4f7a9fb9af2f4eac803a7b1b597ce5e0091f4210 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:36:15 +0200 Subject: Use actor mention instead of username in nomination string --- bot/exts/moderation/watchchannels/talentpool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index 938720cc0..55c41a754 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -271,7 +271,9 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): reason = site_entry["reason"] or "*None*" created = time.format_infraction(site_entry["inserted_at"]) - entries.append(f"Actor: {actor or actor_id}\nReason: {reason}\nCreated: {created}") + entries.append( + f"Actor: {actor.mention if actor else actor_id}\nReason: {reason}\nCreated: {created}" + ) entries_string = "\n\n".join(entries) -- cgit v1.2.3 From 96a369cf0922f3839c20c0c4c62f9fafb8f8ba9f Mon Sep 17 00:00:00 2001 From: Steele Farnsworth <32915757+swfarnsworth@users.noreply.github.com> Date: Sat, 6 Mar 2021 16:27:21 -0500 Subject: Made multiline concatenated string conform to a certain style. That style is not currently enforced by the linter. Co-authored-by: Matteo Bertucci --- bot/exts/moderation/infraction/_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index e58c2b22f..a98b4828b 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -32,8 +32,10 @@ APPEAL_EMAIL = "appeals@pythondiscord.com" INFRACTION_TITLE = "Please review our rules" INFRACTION_APPEAL_EMAIL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}" -INFRACTION_APPEAL_MODMAIL_FOOTER = ('If you would like to discuss or appeal this infraction, ' - 'send a message to the ModMail bot') +INFRACTION_APPEAL_MODMAIL_FOOTER = ( + 'If you would like to discuss or appeal this infraction, ' + 'send a message to the ModMail bot' +) INFRACTION_AUTHOR_NAME = "Infraction information" INFRACTION_DESCRIPTION_TEMPLATE = ( -- cgit v1.2.3 From fa016c096ef249ea1b8d722633882a09535e9c44 Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 11 Feb 2021 01:51:40 -0800 Subject: Added filter. --- bot/exts/info/pypi.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 3e326e8bb..8fe249c8a 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -1,6 +1,7 @@ import itertools import logging import random +import re from discord import Embed from discord.ext.commands import Cog, Context, command @@ -12,8 +13,11 @@ from bot.constants import Colours, NEGATIVE_REPLIES URL = "https://pypi.org/pypi/{package}/json" FIELDS = ("author", "requires_python", "summary", "license") PYPI_ICON = "https://cdn.discordapp.com/emojis/766274397257334814.png" + PYPI_COLOURS = itertools.cycle((Colours.yellow, Colours.blue, Colours.white)) +ILLEGAL_CHARACTERS = re.compile(r"[^a-zA-Z0-9-.]+") + log = logging.getLogger(__name__) @@ -32,6 +36,11 @@ class PyPi(Cog): ) embed.set_thumbnail(url=PYPI_ICON) + if (character := re.search(ILLEGAL_CHARACTERS, package)) is not None: + embed.description = f"Illegal character passed into command: '{escape_markdown(character.group(0))}'" + await ctx.send(embed=embed) + return + async with self.bot.http_session.get(URL.format(package=package)) as response: if response.status == 404: embed.description = "Package could not be found." -- cgit v1.2.3 From cd71b8447eaad67b6885d99e00c230198c21cf0e Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sun, 7 Mar 2021 16:27:17 +0100 Subject: Mark #appeals as a mod channel --- config-default.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config-default.yml b/config-default.yml index 18d9cd370..3dbc7bd6b 100644 --- a/config-default.yml +++ b/config-default.yml @@ -195,6 +195,7 @@ guild: incidents_archive: 720668923636351037 mods: &MODS 305126844661760000 mod_alerts: 473092532147060736 + mod_appeals: &MOD_APPEALS 808790025688711198 mod_meta: &MOD_META 775412552795947058 mod_spam: &MOD_SPAM 620607373828030464 mod_tools: &MOD_TOOLS 775413915391098921 @@ -230,6 +231,7 @@ guild: moderation_channels: - *ADMINS - *ADMIN_SPAM + - *MOD_APPEALS - *MOD_META - *MOD_TOOLS - *MODS -- cgit v1.2.3 From 75df6d9ac952c76260fd44f5191c02423bb847fa Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 7 Mar 2021 18:56:18 +0200 Subject: Improve nomination string representation --- bot/exts/moderation/watchchannels/talentpool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index 55c41a754..c2f6ab2c5 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -272,7 +272,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): reason = site_entry["reason"] or "*None*" created = time.format_infraction(site_entry["inserted_at"]) entries.append( - f"Actor: {actor.mention if actor else actor_id}\nReason: {reason}\nCreated: {created}" + f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" ) entries_string = "\n\n".join(entries) @@ -299,12 +299,12 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): =============== Status: Inactive Date: {start_date} + Nomination ID: `{nomination_object["id"]}` {entries_string} End date: {end_date} Unwatch reason: {nomination_object["end_reason"]} - Nomination ID: `{nomination_object["id"]}` =============== """ ) -- cgit v1.2.3 From 1255bbebce25f82620af7c0c52ed70905c37ea53 Mon Sep 17 00:00:00 2001 From: xithrius Date: Mon, 8 Mar 2021 03:26:56 -0800 Subject: Purge ban now says 'purge ban' on user purge ban. --- bot/exts/moderation/infraction/_scheduler.py | 6 ++++-- bot/exts/moderation/infraction/infractions.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index a73f2e8da..b48c1c19e 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -173,6 +173,8 @@ class InfractionScheduler: total = len(infractions) end_msg = f" (#{id_} ; {total} infraction{ngettext('', 's', total)} total)" + purge = infraction['purge'] + # Execute the necessary actions to apply the infraction on Discord. if action_coro: log.trace(f"Awaiting the infraction #{id_} application action coroutine.") @@ -210,7 +212,7 @@ class InfractionScheduler: log.error(f"Deletion of {infr_type} infraction #{id_} failed with error code {e.status}.") infr_message = "" else: - infr_message = f" **{' '.join(infr_type.split('_'))}** to {user.mention}{expiry_msg}{end_msg}" + infr_message = f" **{purge}{' '.join(infr_type.split('_'))}** to {user.mention}{expiry_msg}{end_msg}" # Send a confirmation message to the invoking context. log.trace(f"Sending infraction #{id_} confirmation message.") @@ -234,7 +236,7 @@ class InfractionScheduler: footer=f"ID {infraction['id']}" ) - log.info(f"Applied {infr_type} infraction #{id_} to {user}.") + log.info(f"Applied {purge}{infr_type} infraction #{id_} to {user}.") return not failed async def pardon_infraction( diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 3b5b1df45..d89e80acc 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -318,6 +318,8 @@ class Infractions(InfractionScheduler, commands.Cog): if infraction is None: return + infraction["purge"] = "purge " if purge_days else "" + self.mod_log.ignore(Event.member_remove, user.id) if reason: -- cgit v1.2.3 From f76e47bf9b1d9956a36d891e3aa64593c65568c8 Mon Sep 17 00:00:00 2001 From: xithrius Date: Mon, 8 Mar 2021 03:35:54 -0800 Subject: Fixed unittest for purge infraction. --- tests/bot/exts/moderation/infraction/test_infractions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 86c2617ea..08f39cd50 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -39,7 +39,7 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase): delete_message_days=0 ) self.cog.apply_infraction.assert_awaited_once_with( - self.ctx, {"foo": "bar"}, self.target, self.ctx.guild.ban.return_value + self.ctx, {"foo": "bar", "purge": ""}, self.target, self.ctx.guild.ban.return_value ) @patch("bot.exts.moderation.infraction._utils.post_infraction") -- cgit v1.2.3 From 7a97eec931a8eb72ff1aac101e5bdd8e5b51de62 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 8 Mar 2021 17:08:07 +0100 Subject: Make the snowflake command accept many snowflakes --- bot/exts/utils/utils.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index eb92dfca7..1a5ded7a8 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -2,7 +2,7 @@ import difflib import logging import re import unicodedata -from typing import Tuple, Union +from typing import Tuple, Union, List from discord import Colour, Embed, utils from discord.ext.commands import BadArgument, Cog, Context, clean_content, command, has_any_role @@ -156,18 +156,19 @@ class Utils(Cog): @command(aliases=("snf", "snfl", "sf")) @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES) - async def snowflake(self, ctx: Context, snowflake: Snowflake) -> None: + async def snowflake(self, ctx: Context, *snowflakes: Snowflake) -> None: """Get Discord snowflake creation time.""" - created_at = snowflake_time(snowflake) - embed = Embed( - description=f"**Created at {created_at}** ({time_since(created_at, max_units=3)}).", - colour=Colour.blue() - ) - embed.set_author( - name=f"Snowflake: {snowflake}", - icon_url="https://github.com/twitter/twemoji/blob/master/assets/72x72/2744.png?raw=true" - ) - await ctx.send(embed=embed) + for snowflake in snowflakes: + created_at = snowflake_time(snowflake) + embed = Embed( + description=f"**Created at {created_at}** ({time_since(created_at, max_units=3)}).", + colour=Colour.blue() + ) + embed.set_author( + name=f"Snowflake: {snowflake}", + icon_url="https://github.com/twitter/twemoji/blob/master/assets/72x72/2744.png?raw=true" + ) + await ctx.send(embed=embed) @command(aliases=("poll",)) @has_any_role(*MODERATION_ROLES) -- cgit v1.2.3 From 0a2e08c28d0dc6ca523bdf421a4759d9c38d8a3f Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Mon, 8 Mar 2021 17:30:58 +0100 Subject: Restrict non-staffer to one snowflake at the time --- bot/exts/utils/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 1a5ded7a8..a5d6f69b9 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -2,7 +2,7 @@ import difflib import logging import re import unicodedata -from typing import Tuple, Union, List +from typing import Tuple, Union from discord import Colour, Embed, utils from discord.ext.commands import BadArgument, Cog, Context, clean_content, command, has_any_role @@ -14,6 +14,7 @@ from bot.converters import Snowflake from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils import messages +from bot.utils.checks import has_no_roles_check from bot.utils.time import time_since log = logging.getLogger(__name__) @@ -158,6 +159,9 @@ class Utils(Cog): @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES) async def snowflake(self, ctx: Context, *snowflakes: Snowflake) -> None: """Get Discord snowflake creation time.""" + if len(snowflakes) > 1 and await has_no_roles_check(ctx, *STAFF_ROLES): + raise BadArgument("Cannot process more than one snowflake in one invocation.") + for snowflake in snowflakes: created_at = snowflake_time(snowflake) embed = Embed( -- cgit v1.2.3 From ecdffd57c5a51143706d4fdc129645901352abb6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 8 Mar 2021 19:18:28 +0200 Subject: Don't mention watching anymore in talent pool add message --- bot/exts/moderation/watchchannels/talentpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index c2f6ab2c5..737ee684d 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -109,7 +109,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): resp.raise_for_status() self.watched_users[user.id] = response_data - msg = f":white_check_mark: Messages sent by {user} will now be relayed to the talent pool channel" + msg = f":white_check_mark: The nomination for {user} has been added to the talent pool" history = await self.bot.api_client.get( self.api_endpoint, -- cgit v1.2.3 From fa93d2fd8ed03fb991bf32573e67b49e89c56057 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 8 Mar 2021 19:21:16 +0200 Subject: Shorten reason of nomination string to 1000 characters --- bot/exts/moderation/watchchannels/talentpool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index 737ee684d..49221002e 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -269,7 +269,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): actor_id = site_entry["actor"] actor = guild.get_member(actor_id) - reason = site_entry["reason"] or "*None*" + reason = textwrap.shorten(site_entry["reason"], 1000, placeholder="...") or "*None*" created = time.format_infraction(site_entry["inserted_at"]) entries.append( f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" -- cgit v1.2.3 From 7d3d3eaa6474902f120a94b885d4b4b789c2b87c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Mon, 8 Mar 2021 19:33:34 +0200 Subject: Limit maximum characters for reasons to 1000 --- bot/exts/moderation/watchchannels/talentpool.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py index 49221002e..d75688fa6 100644 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ b/bot/exts/moderation/watchchannels/talentpool.py @@ -14,6 +14,8 @@ from bot.exts.moderation.watchchannels._watchchannel import WatchChannel from bot.pagination import LinePaginator from bot.utils import time +REASON_MAX_CHARS = 1000 + log = logging.getLogger(__name__) @@ -84,6 +86,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await ctx.send(f":x: Failed to update the user cache; can't add {user}") return + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") + return + # Manual request with `raise_for_status` as False because we want the actual response session = self.bot.api_client.session url = self.bot.api_client._url_for(self.api_endpoint) @@ -162,6 +168,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): Providing a `reason` is required. """ + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") + return + if await self.unwatch(user.id, reason): await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed") else: @@ -177,6 +187,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): @has_any_role(*MODERATION_ROLES) async def edit_reason_command(self, ctx: Context, nomination_id: int, actor: FetchedMember, *, reason: str) -> None: """Edits the reason of a specific nominator in a specific active nomination.""" + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") + return + try: nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") except ResponseCodeError as e: @@ -208,6 +222,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): @has_any_role(*MODERATION_ROLES) async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: """Edits the unnominate reason for the nomination with the given `id`.""" + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") + return + try: nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") except ResponseCodeError as e: @@ -269,7 +287,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): actor_id = site_entry["actor"] actor = guild.get_member(actor_id) - reason = textwrap.shorten(site_entry["reason"], 1000, placeholder="...") or "*None*" + reason = site_entry["reason"] or "*None*" created = time.format_infraction(site_entry["inserted_at"]) entries.append( f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" -- cgit v1.2.3 From 1ebc283ce03cd4beec562123a536bff58278e2ca Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 8 Mar 2021 20:55:19 +0300 Subject: Revert "Use JSON logging in production" --- Pipfile | 1 - Pipfile.lock | 260 ++++++++++++++++++++++------------------------------------- bot/log.py | 49 ++++------- 3 files changed, 114 insertions(+), 196 deletions(-) diff --git a/Pipfile b/Pipfile index 024aa6eff..0a94fb888 100644 --- a/Pipfile +++ b/Pipfile @@ -28,7 +28,6 @@ sphinx = "~=2.2" statsd = "~=3.3" arrow = "~=0.17" emoji = "~=0.6" -python-json-logger = "~=2.0" [dev-packages] coverage = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index dc7f6f21f..f8cedb08f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "81ca9d1891e71de1c3f71958f082e1a8cad71e5b3ca425dc561d0ae74664fdb0" + "sha256": "228ae55fe5700ac3827ba6b661933b60b1d06f44fea8bcbe8c5a769fa10ab2fd" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "aio-pika": { "hashes": [ - "sha256:1d4305a5f78af3857310b4fe48348cdcf6c097e0e275ea88c2cd08570531a369", - "sha256:e69afef8695f47c5d107bbdba21bdb845d5c249acb3be53ef5c2d497b02657c0" + "sha256:9773440a89840941ac3099a7720bf9d51e8764a484066b82ede4d395660ff430", + "sha256:a8065be3c722eb8f9fff8c0e7590729e7782202cdb9363d9830d7d5d47b45c7c" ], "index": "pypi", - "version": "==6.8.0" + "version": "==6.7.1" }, "aiodns": { "hashes": [ @@ -96,7 +96,6 @@ "sha256:8218dd9f7198d6e7935855468326bbacf0089f926c70baa8dd92944cb2496573", "sha256:e584dac13a242589aaf42470fd3006cb0dc5aed6506cbd20357c7ec8bbe4a89e" ], - "markers": "python_version >= '3.6'", "version": "==3.3.1" }, "alabaster": { @@ -123,7 +122,6 @@ "sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af" ], "index": "pypi", - "markers": "python_version ~= '3.7'", "version": "==0.1.4" }, "async-timeout": { @@ -131,7 +129,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -139,7 +136,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "babel": { @@ -147,7 +143,6 @@ "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0" }, "beautifulsoup4": { @@ -220,6 +215,7 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==0.4.4" }, @@ -252,7 +248,6 @@ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, "emoji": { @@ -335,7 +330,6 @@ "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "humanfriendly": { @@ -343,7 +337,6 @@ "sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d", "sha256:d5c731705114b9ad673754f3317d9fa4c23212f36b29bdc4272a892eafc9bc72" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==9.1" }, "idna": { @@ -351,7 +344,6 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "imagesize": { @@ -359,7 +351,6 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "jinja2": { @@ -367,7 +358,6 @@ "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.3" }, "lxml": { @@ -476,16 +466,15 @@ "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "more-itertools": { "hashes": [ - "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", - "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" + "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330", + "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf" ], "index": "pypi", - "version": "==8.7.0" + "version": "==8.6.0" }, "multidict": { "hashes": [ @@ -527,14 +516,12 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], - "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "ordered-set": { "hashes": [ "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95" ], - "markers": "python_version >= '3.5'", "version": "==4.0.2" }, "packaging": { @@ -542,7 +529,6 @@ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.9" }, "pamqp": { @@ -591,7 +577,6 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pygments": { @@ -599,7 +584,6 @@ "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" ], - "markers": "python_version >= '3.5'", "version": "==2.8.0" }, "pyparsing": { @@ -607,7 +591,6 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "python-dateutil": { @@ -618,13 +601,6 @@ "index": "pypi", "version": "==2.8.1" }, - "python-json-logger": { - "hashes": [ - "sha256:f26eea7898db40609563bed0a7ca11af12e2a79858632706d835a0f961b7d398" - ], - "index": "pypi", - "version": "==2.0.1" - }, "pytz": { "hashes": [ "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", @@ -634,37 +610,28 @@ }, "pyyaml": { "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], "index": "pypi", - "version": "==5.4.1" + "version": "==5.3.1" }, "redis": { "hashes": [ "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.5.3" }, "requests": { @@ -677,18 +644,17 @@ }, "sentry-sdk": { "hashes": [ - "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237", - "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b" + "sha256:0a711ec952441c2ec89b8f5d226c33bc697914f46e876b44a4edd3e7864cf4d0", + "sha256:737a094e49a529dd0fdcaafa9e97cf7c3d5eb964bd229821d640bc77f3502b3f" ], "index": "pypi", - "version": "==0.20.3" + "version": "==0.19.5" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -726,7 +692,6 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -734,7 +699,6 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -742,7 +706,6 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -750,7 +713,6 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], - "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -758,7 +720,6 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -766,7 +727,6 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], - "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "statsd": { @@ -790,7 +750,6 @@ "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.3" }, "yarl": { @@ -833,7 +792,6 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "markers": "python_version >= '3.6'", "version": "==1.6.3" } }, @@ -850,7 +808,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "certifi": { @@ -865,7 +822,6 @@ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], - "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -877,61 +833,58 @@ }, "coverage": { "hashes": [ - "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", - "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", - "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", - "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", - "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", - "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", - "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", - "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", - "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", - "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", - "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", - "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", - "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", - "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", - "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", - "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", - "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", - "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", - "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", - "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", - "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", - "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", - "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", - "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", - "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", - "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", - "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", - "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", - "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", - "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", - "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", - "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", - "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", - "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", - "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", - "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", - "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", - "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", - "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", - "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", - "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", - "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", - "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", - "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", - "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", - "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", - "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", - "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", - "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", - "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", - "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", - "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297", + "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1", + "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497", + "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606", + "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528", + "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b", + "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4", + "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830", + "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1", + "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f", + "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d", + "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3", + "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8", + "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500", + "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7", + "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb", + "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b", + "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059", + "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b", + "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72", + "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36", + "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277", + "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c", + "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631", + "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff", + "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8", + "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec", + "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b", + "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7", + "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105", + "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b", + "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c", + "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b", + "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98", + "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4", + "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879", + "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f", + "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4", + "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044", + "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e", + "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899", + "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f", + "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448", + "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714", + "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2", + "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d", + "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd", + "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7", + "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae" ], "index": "pypi", - "version": "==5.5" + "version": "==5.3.1" }, "coveralls": { "hashes": [ @@ -971,11 +924,11 @@ }, "flake8-annotations": { "hashes": [ - "sha256:8968ff12f296433028ad561c680ccc03a7cd62576d100c3f1475e058b3c11b43", - "sha256:bd0505616c0d85ebb45c6052d339c69f320d3f87fa079ab4e91a4f234a863d05" + "sha256:3a377140556aecf11fa9f3bb18c10db01f5ea56dc79a730e2ec9b4f1f49e2055", + "sha256:e17947a48a5b9f632fe0c72682fc797c385e451048e7dfb20139f448a074cb3e" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.5.0" }, "flake8-bugbear": { "hashes": [ @@ -1033,18 +986,16 @@ }, "identify": { "hashes": [ - "sha256:2179e7359471ab55729f201b3fdf7dc2778e221f868410fedcb0987b791ba552", - "sha256:2a5fdf2f5319cc357eda2550bea713a404392495961022cf2462624ce62f0f46" + "sha256:de7129142a5c86d75a52b96f394d94d96d497881d2aaf8eafe320cdbe8ac4bcc", + "sha256:e0dae57c0397629ce13c289f6ddde0204edf518f557bfdb1e56474aa143e77c3" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==2.1.0" + "version": "==1.5.14" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "mccabe": { @@ -1071,18 +1022,17 @@ }, "pre-commit": { "hashes": [ - "sha256:16212d1fde2bed88159287da88ff03796863854b04dc9f838a55979325a3d20e", - "sha256:399baf78f13f4de82a29b649afd74bef2c4e28eb4f021661fc7f29246e8c7a3a" + "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0", + "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4" ], "index": "pypi", - "version": "==2.10.1" + "version": "==2.9.3" }, "pycodestyle": { "hashes": [ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -1090,7 +1040,6 @@ "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], - "markers": "python_version >= '3.5'", "version": "==5.1.1" }, "pyflakes": { @@ -1098,35 +1047,26 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pyyaml": { "hashes": [ - "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", - "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", - "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", - "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", - "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", - "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", - "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", - "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", - "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", - "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", - "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", - "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", - "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", - "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", - "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", - "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", - "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", - "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", - "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", - "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], "index": "pypi", - "version": "==5.4.1" + "version": "==5.3.1" }, "requests": { "hashes": [ @@ -1141,7 +1081,6 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -1156,7 +1095,6 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "urllib3": { @@ -1164,7 +1102,6 @@ "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.3" }, "virtualenv": { @@ -1172,7 +1109,6 @@ "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d", "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4.2" } } diff --git a/bot/log.py b/bot/log.py index bc3bba0af..e92233a33 100644 --- a/bot/log.py +++ b/bot/log.py @@ -1,12 +1,11 @@ import logging import os import sys -from logging import Logger, StreamHandler, handlers +from logging import Logger, handlers from pathlib import Path import coloredlogs import sentry_sdk -from pythonjsonlogger import jsonlogger from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.redis import RedisIntegration @@ -14,15 +13,6 @@ from bot import constants TRACE_LEVEL = 5 -PROD_FIELDS = [ - "asctime", - "name", - "levelname", - "message", - "funcName", - "filename" -] - def setup() -> None: """Set up loggers.""" @@ -43,28 +33,21 @@ def setup() -> None: root_log.setLevel(log_level) root_log.addHandler(file_handler) - if constants.DEBUG_MODE: - if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: - coloredlogs.DEFAULT_LEVEL_STYLES = { - **coloredlogs.DEFAULT_LEVEL_STYLES, - "trace": {"color": 246}, - "critical": {"background": "red"}, - "debug": coloredlogs.DEFAULT_LEVEL_STYLES["info"] - } - - if "COLOREDLOGS_LOG_FORMAT" not in os.environ: - coloredlogs.DEFAULT_LOG_FORMAT = format_string - - if "COLOREDLOGS_LOG_LEVEL" not in os.environ: - coloredlogs.DEFAULT_LOG_LEVEL = log_level - - coloredlogs.install(logger=root_log, stream=sys.stdout) - else: - json_format = " ".join([f"%({field})s" for field in PROD_FIELDS]) - stream_handler = StreamHandler() - formatter = jsonlogger.JsonFormatter(json_format) - stream_handler.setFormatter(formatter) - root_log.addHandler(stream_handler) + if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: + coloredlogs.DEFAULT_LEVEL_STYLES = { + **coloredlogs.DEFAULT_LEVEL_STYLES, + "trace": {"color": 246}, + "critical": {"background": "red"}, + "debug": coloredlogs.DEFAULT_LEVEL_STYLES["info"] + } + + if "COLOREDLOGS_LOG_FORMAT" not in os.environ: + coloredlogs.DEFAULT_LOG_FORMAT = format_string + + if "COLOREDLOGS_LOG_LEVEL" not in os.environ: + coloredlogs.DEFAULT_LOG_LEVEL = log_level + + coloredlogs.install(logger=root_log, stream=sys.stdout) logging.getLogger("discord").setLevel(logging.WARNING) logging.getLogger("websockets").setLevel(logging.WARNING) -- cgit v1.2.3 From bf5efea00f7409e46c5add14bb01c983ff849f2e Mon Sep 17 00:00:00 2001 From: xithrius Date: Mon, 8 Mar 2021 11:56:05 -0800 Subject: Resolving KeyError on infractions that don't purge. --- bot/exts/moderation/infraction/_scheduler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index b48c1c19e..988fb7220 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -173,7 +173,7 @@ class InfractionScheduler: total = len(infractions) end_msg = f" (#{id_} ; {total} infraction{ngettext('', 's', total)} total)" - purge = infraction['purge'] + purge = infraction.get("purge", "") # Execute the necessary actions to apply the infraction on Discord. if action_coro: -- cgit v1.2.3 From 1c4e4387abc3e9cc9f4320457c5d6456cdce7a3f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 9 Mar 2021 13:17:32 +0000 Subject: DevOps team reviews for bot deployments --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5a4aede30..0caf02308 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,6 +10,7 @@ on: jobs: build: + environment: production if: github.event.workflow_run.conclusion == 'success' name: Build & Push runs-on: ubuntu-latest -- cgit v1.2.3 From b7d3419599b503198443dbef04ea9fd1d445108c Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Tue, 9 Mar 2021 15:06:24 +0100 Subject: Fix typo in stars.json Please have a bit of respect to the baguette land. Also this is a good way to test the new deploy approval system. --- bot/resources/stars.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/stars.json b/bot/resources/stars.json index c0b253120..5ecad0213 100644 --- a/bot/resources/stars.json +++ b/bot/resources/stars.json @@ -17,7 +17,7 @@ "Bruce Springsteen", "Bruno Mars", "Bryan Adams", - "Celine Dion", + "Céline Dion", "Cher", "Christina Aguilera", "David Bowie", -- cgit v1.2.3 From 5a8cbaac5a91bfa83a4971961b87cf676b555f50 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 9 Mar 2021 14:13:06 +0000 Subject: Delete repo specific FUNDING.yml file in favour of org one in python-discord/.github --- .github/FUNDING.yml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 6d9919ef2..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -patreon: python_discord -custom: https://www.redbubble.com/people/pythondiscord -- cgit v1.2.3 From 6f880bbc40049948f71af14723f46533fb8c4f1f Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Wed, 10 Mar 2021 20:06:18 +0200 Subject: Moved talentpool to a new recruitment extension --- bot/exts/moderation/watchchannels/talentpool.py | 335 ------------------------ bot/exts/recruitment/talentpool/talentpool.py | 335 ++++++++++++++++++++++++ 2 files changed, 335 insertions(+), 335 deletions(-) delete mode 100644 bot/exts/moderation/watchchannels/talentpool.py create mode 100644 bot/exts/recruitment/talentpool/talentpool.py diff --git a/bot/exts/moderation/watchchannels/talentpool.py b/bot/exts/moderation/watchchannels/talentpool.py deleted file mode 100644 index d75688fa6..000000000 --- a/bot/exts/moderation/watchchannels/talentpool.py +++ /dev/null @@ -1,335 +0,0 @@ -import logging -import textwrap -from collections import ChainMap -from typing import Union - -from discord import Color, Embed, Member, User -from discord.ext.commands import Cog, Context, group, has_any_role - -from bot.api import ResponseCodeError -from bot.bot import Bot -from bot.constants import Channels, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks -from bot.converters import FetchedMember -from bot.exts.moderation.watchchannels._watchchannel import WatchChannel -from bot.pagination import LinePaginator -from bot.utils import time - -REASON_MAX_CHARS = 1000 - -log = logging.getLogger(__name__) - - -class TalentPool(WatchChannel, Cog, name="Talentpool"): - """Relays messages of helper candidates to a watch channel to observe them.""" - - def __init__(self, bot: Bot) -> None: - super().__init__( - bot, - destination=Channels.talent_pool, - webhook_id=Webhooks.talent_pool, - api_endpoint='bot/nominations', - api_default_params={'active': 'true', 'ordering': '-inserted_at'}, - logger=log, - disable_header=True, - ) - - @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) - @has_any_role(*MODERATION_ROLES) - async def nomination_group(self, ctx: Context) -> None: - """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" - await ctx.send_help(ctx.command) - - @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) - @has_any_role(*MODERATION_ROLES) - async def watched_command( - self, ctx: Context, oldest_first: bool = False, update_cache: bool = True - ) -> None: - """ - Shows the users that are currently being monitored in the talent pool. - - The optional kwarg `oldest_first` can be used to order the list by oldest nomination. - - The optional kwarg `update_cache` can be used to update the user - cache using the API before listing the users. - """ - await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache) - - @nomination_group.command(name='oldest') - @has_any_role(*MODERATION_ROLES) - async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None: - """ - Shows talent pool monitored users ordered by oldest nomination. - - The optional kwarg `update_cache` can be used to update the user - cache using the API before listing the users. - """ - await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache) - - @nomination_group.command(name='watch', aliases=('w', 'add', 'a'), root_aliases=("nominate",)) - @has_any_role(*STAFF_ROLES) - async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str = '') -> None: - """ - Relay messages sent by the given `user` to the `#talent-pool` channel. - - A `reason` for adding the user to the talent pool is optional. - If given, it will be displayed in the header when relaying messages of this user to the channel. - """ - if user.bot: - await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.") - return - - if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles): - await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:") - return - - if not await self.fetch_user_cache(): - await ctx.send(f":x: Failed to update the user cache; can't add {user}") - return - - if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") - return - - # Manual request with `raise_for_status` as False because we want the actual response - session = self.bot.api_client.session - url = self.bot.api_client._url_for(self.api_endpoint) - kwargs = { - 'json': { - 'actor': ctx.author.id, - 'reason': reason, - 'user': user.id - }, - 'raise_for_status': False, - } - async with session.post(url, **kwargs) as resp: - response_data = await resp.json() - - if resp.status == 400: - if response_data.get('user', False): - await ctx.send(":x: The specified user can't be found in the database tables") - elif response_data.get('actor', False): - await ctx.send(":x: You have already nominated this user") - - return - else: - resp.raise_for_status() - - self.watched_users[user.id] = response_data - msg = f":white_check_mark: The nomination for {user} has been added to the talent pool" - - history = await self.bot.api_client.get( - self.api_endpoint, - params={ - "user__id": str(user.id), - "active": "false", - "ordering": "-inserted_at" - } - ) - - if history: - msg += f"\n\n({len(history)} previous nominations in total)" - - await ctx.send(msg) - - @nomination_group.command(name='history', aliases=('info', 'search')) - @has_any_role(*MODERATION_ROLES) - async def history_command(self, ctx: Context, user: FetchedMember) -> None: - """Shows the specified user's nomination history.""" - result = await self.bot.api_client.get( - self.api_endpoint, - params={ - 'user__id': str(user.id), - 'ordering': "-active,-inserted_at" - } - ) - if not result: - await ctx.send(":warning: This user has never been nominated") - return - - embed = Embed( - title=f"Nominations for {user.display_name} `({user.id})`", - color=Color.blue() - ) - lines = [self._nomination_to_string(nomination) for nomination in result] - await LinePaginator.paginate( - lines, - ctx=ctx, - embed=embed, - empty=True, - max_lines=3, - max_size=1000 - ) - - @nomination_group.command(name='unwatch', aliases=('end', ), root_aliases=("unnominate",)) - @has_any_role(*MODERATION_ROLES) - async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None: - """ - Ends the active nomination of the specified user with the given reason. - - Providing a `reason` is required. - """ - if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") - return - - if await self.unwatch(user.id, reason): - await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed") - else: - await ctx.send(":x: The specified user does not have an active nomination") - - @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True) - @has_any_role(*MODERATION_ROLES) - async def nomination_edit_group(self, ctx: Context) -> None: - """Commands to edit nominations.""" - await ctx.send_help(ctx.command) - - @nomination_edit_group.command(name='reason') - @has_any_role(*MODERATION_ROLES) - async def edit_reason_command(self, ctx: Context, nomination_id: int, actor: FetchedMember, *, reason: str) -> None: - """Edits the reason of a specific nominator in a specific active nomination.""" - if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") - return - - try: - nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") - except ResponseCodeError as e: - if e.response.status == 404: - self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") - await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") - return - else: - raise - - if not nomination["active"]: - await ctx.send(":x: Can't edit the reason of an inactive nomination.") - return - - if not any(entry["actor"] == actor.id for entry in nomination["entries"]): - await ctx.send(f":x: {actor} doesn't have an entry in this nomination.") - return - - self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}") - - await self.bot.api_client.patch( - f"{self.api_endpoint}/{nomination_id}", - json={"actor": actor.id, "reason": reason} - ) - await self.fetch_user_cache() # Update cache - await ctx.send(":white_check_mark: Successfully updated nomination reason.") - - @nomination_edit_group.command(name='end_reason') - @has_any_role(*MODERATION_ROLES) - async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: - """Edits the unnominate reason for the nomination with the given `id`.""" - if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") - return - - try: - nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") - except ResponseCodeError as e: - if e.response.status == 404: - self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") - await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") - return - else: - raise - - if nomination["active"]: - await ctx.send(":x: Can't edit the end reason of an active nomination.") - return - - self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}") - - await self.bot.api_client.patch( - f"{self.api_endpoint}/{nomination_id}", - json={"end_reason": reason} - ) - await self.fetch_user_cache() # Update cache. - await ctx.send(":white_check_mark: Updated the end reason of the nomination!") - - @Cog.listener() - async def on_member_ban(self, guild: Guild, user: Union[User, Member]) -> None: - """Remove `user` from the talent pool after they are banned.""" - await self.unwatch(user.id, "User was banned.") - - async def unwatch(self, user_id: int, reason: str) -> bool: - """End the active nomination of a user with the given reason and return True on success.""" - active_nomination = await self.bot.api_client.get( - self.api_endpoint, - params=ChainMap( - {"user__id": str(user_id)}, - self.api_default_params, - ) - ) - - if not active_nomination: - log.debug(f"No active nominate exists for {user_id=}") - return False - - log.info(f"Ending nomination: {user_id=} {reason=}") - - nomination = active_nomination[0] - await self.bot.api_client.patch( - f"{self.api_endpoint}/{nomination['id']}", - json={'end_reason': reason, 'active': False} - ) - self._remove_user(user_id) - - return True - - def _nomination_to_string(self, nomination_object: dict) -> str: - """Creates a string representation of a nomination.""" - guild = self.bot.get_guild(Guild.id) - entries = [] - for site_entry in nomination_object["entries"]: - actor_id = site_entry["actor"] - actor = guild.get_member(actor_id) - - reason = site_entry["reason"] or "*None*" - created = time.format_infraction(site_entry["inserted_at"]) - entries.append( - f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" - ) - - entries_string = "\n\n".join(entries) - - active = nomination_object["active"] - - start_date = time.format_infraction(nomination_object["inserted_at"]) - if active: - lines = textwrap.dedent( - f""" - =============== - Status: **Active** - Date: {start_date} - Nomination ID: `{nomination_object["id"]}` - - {entries_string} - =============== - """ - ) - else: - end_date = time.format_infraction(nomination_object["ended_at"]) - lines = textwrap.dedent( - f""" - =============== - Status: Inactive - Date: {start_date} - Nomination ID: `{nomination_object["id"]}` - - {entries_string} - - End date: {end_date} - Unwatch reason: {nomination_object["end_reason"]} - =============== - """ - ) - - return lines.strip() - - -def setup(bot: Bot) -> None: - """Load the TalentPool cog.""" - bot.add_cog(TalentPool(bot)) diff --git a/bot/exts/recruitment/talentpool/talentpool.py b/bot/exts/recruitment/talentpool/talentpool.py new file mode 100644 index 000000000..d75688fa6 --- /dev/null +++ b/bot/exts/recruitment/talentpool/talentpool.py @@ -0,0 +1,335 @@ +import logging +import textwrap +from collections import ChainMap +from typing import Union + +from discord import Color, Embed, Member, User +from discord.ext.commands import Cog, Context, group, has_any_role + +from bot.api import ResponseCodeError +from bot.bot import Bot +from bot.constants import Channels, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks +from bot.converters import FetchedMember +from bot.exts.moderation.watchchannels._watchchannel import WatchChannel +from bot.pagination import LinePaginator +from bot.utils import time + +REASON_MAX_CHARS = 1000 + +log = logging.getLogger(__name__) + + +class TalentPool(WatchChannel, Cog, name="Talentpool"): + """Relays messages of helper candidates to a watch channel to observe them.""" + + def __init__(self, bot: Bot) -> None: + super().__init__( + bot, + destination=Channels.talent_pool, + webhook_id=Webhooks.talent_pool, + api_endpoint='bot/nominations', + api_default_params={'active': 'true', 'ordering': '-inserted_at'}, + logger=log, + disable_header=True, + ) + + @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def nomination_group(self, ctx: Context) -> None: + """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" + await ctx.send_help(ctx.command) + + @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) + @has_any_role(*MODERATION_ROLES) + async def watched_command( + self, ctx: Context, oldest_first: bool = False, update_cache: bool = True + ) -> None: + """ + Shows the users that are currently being monitored in the talent pool. + + The optional kwarg `oldest_first` can be used to order the list by oldest nomination. + + The optional kwarg `update_cache` can be used to update the user + cache using the API before listing the users. + """ + await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache) + + @nomination_group.command(name='oldest') + @has_any_role(*MODERATION_ROLES) + async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None: + """ + Shows talent pool monitored users ordered by oldest nomination. + + The optional kwarg `update_cache` can be used to update the user + cache using the API before listing the users. + """ + await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache) + + @nomination_group.command(name='watch', aliases=('w', 'add', 'a'), root_aliases=("nominate",)) + @has_any_role(*STAFF_ROLES) + async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str = '') -> None: + """ + Relay messages sent by the given `user` to the `#talent-pool` channel. + + A `reason` for adding the user to the talent pool is optional. + If given, it will be displayed in the header when relaying messages of this user to the channel. + """ + if user.bot: + await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.") + return + + if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles): + await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:") + return + + if not await self.fetch_user_cache(): + await ctx.send(f":x: Failed to update the user cache; can't add {user}") + return + + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") + return + + # Manual request with `raise_for_status` as False because we want the actual response + session = self.bot.api_client.session + url = self.bot.api_client._url_for(self.api_endpoint) + kwargs = { + 'json': { + 'actor': ctx.author.id, + 'reason': reason, + 'user': user.id + }, + 'raise_for_status': False, + } + async with session.post(url, **kwargs) as resp: + response_data = await resp.json() + + if resp.status == 400: + if response_data.get('user', False): + await ctx.send(":x: The specified user can't be found in the database tables") + elif response_data.get('actor', False): + await ctx.send(":x: You have already nominated this user") + + return + else: + resp.raise_for_status() + + self.watched_users[user.id] = response_data + msg = f":white_check_mark: The nomination for {user} has been added to the talent pool" + + history = await self.bot.api_client.get( + self.api_endpoint, + params={ + "user__id": str(user.id), + "active": "false", + "ordering": "-inserted_at" + } + ) + + if history: + msg += f"\n\n({len(history)} previous nominations in total)" + + await ctx.send(msg) + + @nomination_group.command(name='history', aliases=('info', 'search')) + @has_any_role(*MODERATION_ROLES) + async def history_command(self, ctx: Context, user: FetchedMember) -> None: + """Shows the specified user's nomination history.""" + result = await self.bot.api_client.get( + self.api_endpoint, + params={ + 'user__id': str(user.id), + 'ordering': "-active,-inserted_at" + } + ) + if not result: + await ctx.send(":warning: This user has never been nominated") + return + + embed = Embed( + title=f"Nominations for {user.display_name} `({user.id})`", + color=Color.blue() + ) + lines = [self._nomination_to_string(nomination) for nomination in result] + await LinePaginator.paginate( + lines, + ctx=ctx, + embed=embed, + empty=True, + max_lines=3, + max_size=1000 + ) + + @nomination_group.command(name='unwatch', aliases=('end', ), root_aliases=("unnominate",)) + @has_any_role(*MODERATION_ROLES) + async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None: + """ + Ends the active nomination of the specified user with the given reason. + + Providing a `reason` is required. + """ + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") + return + + if await self.unwatch(user.id, reason): + await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed") + else: + await ctx.send(":x: The specified user does not have an active nomination") + + @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def nomination_edit_group(self, ctx: Context) -> None: + """Commands to edit nominations.""" + await ctx.send_help(ctx.command) + + @nomination_edit_group.command(name='reason') + @has_any_role(*MODERATION_ROLES) + async def edit_reason_command(self, ctx: Context, nomination_id: int, actor: FetchedMember, *, reason: str) -> None: + """Edits the reason of a specific nominator in a specific active nomination.""" + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") + return + + try: + nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") + except ResponseCodeError as e: + if e.response.status == 404: + self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") + await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") + return + else: + raise + + if not nomination["active"]: + await ctx.send(":x: Can't edit the reason of an inactive nomination.") + return + + if not any(entry["actor"] == actor.id for entry in nomination["entries"]): + await ctx.send(f":x: {actor} doesn't have an entry in this nomination.") + return + + self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}") + + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination_id}", + json={"actor": actor.id, "reason": reason} + ) + await self.fetch_user_cache() # Update cache + await ctx.send(":white_check_mark: Successfully updated nomination reason.") + + @nomination_edit_group.command(name='end_reason') + @has_any_role(*MODERATION_ROLES) + async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: + """Edits the unnominate reason for the nomination with the given `id`.""" + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") + return + + try: + nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") + except ResponseCodeError as e: + if e.response.status == 404: + self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") + await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") + return + else: + raise + + if nomination["active"]: + await ctx.send(":x: Can't edit the end reason of an active nomination.") + return + + self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}") + + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination_id}", + json={"end_reason": reason} + ) + await self.fetch_user_cache() # Update cache. + await ctx.send(":white_check_mark: Updated the end reason of the nomination!") + + @Cog.listener() + async def on_member_ban(self, guild: Guild, user: Union[User, Member]) -> None: + """Remove `user` from the talent pool after they are banned.""" + await self.unwatch(user.id, "User was banned.") + + async def unwatch(self, user_id: int, reason: str) -> bool: + """End the active nomination of a user with the given reason and return True on success.""" + active_nomination = await self.bot.api_client.get( + self.api_endpoint, + params=ChainMap( + {"user__id": str(user_id)}, + self.api_default_params, + ) + ) + + if not active_nomination: + log.debug(f"No active nominate exists for {user_id=}") + return False + + log.info(f"Ending nomination: {user_id=} {reason=}") + + nomination = active_nomination[0] + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination['id']}", + json={'end_reason': reason, 'active': False} + ) + self._remove_user(user_id) + + return True + + def _nomination_to_string(self, nomination_object: dict) -> str: + """Creates a string representation of a nomination.""" + guild = self.bot.get_guild(Guild.id) + entries = [] + for site_entry in nomination_object["entries"]: + actor_id = site_entry["actor"] + actor = guild.get_member(actor_id) + + reason = site_entry["reason"] or "*None*" + created = time.format_infraction(site_entry["inserted_at"]) + entries.append( + f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" + ) + + entries_string = "\n\n".join(entries) + + active = nomination_object["active"] + + start_date = time.format_infraction(nomination_object["inserted_at"]) + if active: + lines = textwrap.dedent( + f""" + =============== + Status: **Active** + Date: {start_date} + Nomination ID: `{nomination_object["id"]}` + + {entries_string} + =============== + """ + ) + else: + end_date = time.format_infraction(nomination_object["ended_at"]) + lines = textwrap.dedent( + f""" + =============== + Status: Inactive + Date: {start_date} + Nomination ID: `{nomination_object["id"]}` + + {entries_string} + + End date: {end_date} + Unwatch reason: {nomination_object["end_reason"]} + =============== + """ + ) + + return lines.strip() + + +def setup(bot: Bot) -> None: + """Load the TalentPool cog.""" + bot.add_cog(TalentPool(bot)) -- cgit v1.2.3 From 65f93df5388e4c90ddbc985305d14d5120b24863 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Wed, 10 Mar 2021 20:13:18 +0200 Subject: Rename talentpool.py to _cog.py This change is done in preparation to having the cog split across multiple files. --- bot/exts/recruitment/talentpool/_cog.py | 335 ++++++++++++++++++++++++++ bot/exts/recruitment/talentpool/talentpool.py | 335 -------------------------- 2 files changed, 335 insertions(+), 335 deletions(-) create mode 100644 bot/exts/recruitment/talentpool/_cog.py delete mode 100644 bot/exts/recruitment/talentpool/talentpool.py diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py new file mode 100644 index 000000000..d75688fa6 --- /dev/null +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -0,0 +1,335 @@ +import logging +import textwrap +from collections import ChainMap +from typing import Union + +from discord import Color, Embed, Member, User +from discord.ext.commands import Cog, Context, group, has_any_role + +from bot.api import ResponseCodeError +from bot.bot import Bot +from bot.constants import Channels, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks +from bot.converters import FetchedMember +from bot.exts.moderation.watchchannels._watchchannel import WatchChannel +from bot.pagination import LinePaginator +from bot.utils import time + +REASON_MAX_CHARS = 1000 + +log = logging.getLogger(__name__) + + +class TalentPool(WatchChannel, Cog, name="Talentpool"): + """Relays messages of helper candidates to a watch channel to observe them.""" + + def __init__(self, bot: Bot) -> None: + super().__init__( + bot, + destination=Channels.talent_pool, + webhook_id=Webhooks.talent_pool, + api_endpoint='bot/nominations', + api_default_params={'active': 'true', 'ordering': '-inserted_at'}, + logger=log, + disable_header=True, + ) + + @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def nomination_group(self, ctx: Context) -> None: + """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" + await ctx.send_help(ctx.command) + + @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) + @has_any_role(*MODERATION_ROLES) + async def watched_command( + self, ctx: Context, oldest_first: bool = False, update_cache: bool = True + ) -> None: + """ + Shows the users that are currently being monitored in the talent pool. + + The optional kwarg `oldest_first` can be used to order the list by oldest nomination. + + The optional kwarg `update_cache` can be used to update the user + cache using the API before listing the users. + """ + await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache) + + @nomination_group.command(name='oldest') + @has_any_role(*MODERATION_ROLES) + async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None: + """ + Shows talent pool monitored users ordered by oldest nomination. + + The optional kwarg `update_cache` can be used to update the user + cache using the API before listing the users. + """ + await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache) + + @nomination_group.command(name='watch', aliases=('w', 'add', 'a'), root_aliases=("nominate",)) + @has_any_role(*STAFF_ROLES) + async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str = '') -> None: + """ + Relay messages sent by the given `user` to the `#talent-pool` channel. + + A `reason` for adding the user to the talent pool is optional. + If given, it will be displayed in the header when relaying messages of this user to the channel. + """ + if user.bot: + await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.") + return + + if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles): + await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:") + return + + if not await self.fetch_user_cache(): + await ctx.send(f":x: Failed to update the user cache; can't add {user}") + return + + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") + return + + # Manual request with `raise_for_status` as False because we want the actual response + session = self.bot.api_client.session + url = self.bot.api_client._url_for(self.api_endpoint) + kwargs = { + 'json': { + 'actor': ctx.author.id, + 'reason': reason, + 'user': user.id + }, + 'raise_for_status': False, + } + async with session.post(url, **kwargs) as resp: + response_data = await resp.json() + + if resp.status == 400: + if response_data.get('user', False): + await ctx.send(":x: The specified user can't be found in the database tables") + elif response_data.get('actor', False): + await ctx.send(":x: You have already nominated this user") + + return + else: + resp.raise_for_status() + + self.watched_users[user.id] = response_data + msg = f":white_check_mark: The nomination for {user} has been added to the talent pool" + + history = await self.bot.api_client.get( + self.api_endpoint, + params={ + "user__id": str(user.id), + "active": "false", + "ordering": "-inserted_at" + } + ) + + if history: + msg += f"\n\n({len(history)} previous nominations in total)" + + await ctx.send(msg) + + @nomination_group.command(name='history', aliases=('info', 'search')) + @has_any_role(*MODERATION_ROLES) + async def history_command(self, ctx: Context, user: FetchedMember) -> None: + """Shows the specified user's nomination history.""" + result = await self.bot.api_client.get( + self.api_endpoint, + params={ + 'user__id': str(user.id), + 'ordering': "-active,-inserted_at" + } + ) + if not result: + await ctx.send(":warning: This user has never been nominated") + return + + embed = Embed( + title=f"Nominations for {user.display_name} `({user.id})`", + color=Color.blue() + ) + lines = [self._nomination_to_string(nomination) for nomination in result] + await LinePaginator.paginate( + lines, + ctx=ctx, + embed=embed, + empty=True, + max_lines=3, + max_size=1000 + ) + + @nomination_group.command(name='unwatch', aliases=('end', ), root_aliases=("unnominate",)) + @has_any_role(*MODERATION_ROLES) + async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None: + """ + Ends the active nomination of the specified user with the given reason. + + Providing a `reason` is required. + """ + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") + return + + if await self.unwatch(user.id, reason): + await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed") + else: + await ctx.send(":x: The specified user does not have an active nomination") + + @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def nomination_edit_group(self, ctx: Context) -> None: + """Commands to edit nominations.""" + await ctx.send_help(ctx.command) + + @nomination_edit_group.command(name='reason') + @has_any_role(*MODERATION_ROLES) + async def edit_reason_command(self, ctx: Context, nomination_id: int, actor: FetchedMember, *, reason: str) -> None: + """Edits the reason of a specific nominator in a specific active nomination.""" + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") + return + + try: + nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") + except ResponseCodeError as e: + if e.response.status == 404: + self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") + await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") + return + else: + raise + + if not nomination["active"]: + await ctx.send(":x: Can't edit the reason of an inactive nomination.") + return + + if not any(entry["actor"] == actor.id for entry in nomination["entries"]): + await ctx.send(f":x: {actor} doesn't have an entry in this nomination.") + return + + self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}") + + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination_id}", + json={"actor": actor.id, "reason": reason} + ) + await self.fetch_user_cache() # Update cache + await ctx.send(":white_check_mark: Successfully updated nomination reason.") + + @nomination_edit_group.command(name='end_reason') + @has_any_role(*MODERATION_ROLES) + async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: + """Edits the unnominate reason for the nomination with the given `id`.""" + if len(reason) > REASON_MAX_CHARS: + await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") + return + + try: + nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") + except ResponseCodeError as e: + if e.response.status == 404: + self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") + await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") + return + else: + raise + + if nomination["active"]: + await ctx.send(":x: Can't edit the end reason of an active nomination.") + return + + self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}") + + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination_id}", + json={"end_reason": reason} + ) + await self.fetch_user_cache() # Update cache. + await ctx.send(":white_check_mark: Updated the end reason of the nomination!") + + @Cog.listener() + async def on_member_ban(self, guild: Guild, user: Union[User, Member]) -> None: + """Remove `user` from the talent pool after they are banned.""" + await self.unwatch(user.id, "User was banned.") + + async def unwatch(self, user_id: int, reason: str) -> bool: + """End the active nomination of a user with the given reason and return True on success.""" + active_nomination = await self.bot.api_client.get( + self.api_endpoint, + params=ChainMap( + {"user__id": str(user_id)}, + self.api_default_params, + ) + ) + + if not active_nomination: + log.debug(f"No active nominate exists for {user_id=}") + return False + + log.info(f"Ending nomination: {user_id=} {reason=}") + + nomination = active_nomination[0] + await self.bot.api_client.patch( + f"{self.api_endpoint}/{nomination['id']}", + json={'end_reason': reason, 'active': False} + ) + self._remove_user(user_id) + + return True + + def _nomination_to_string(self, nomination_object: dict) -> str: + """Creates a string representation of a nomination.""" + guild = self.bot.get_guild(Guild.id) + entries = [] + for site_entry in nomination_object["entries"]: + actor_id = site_entry["actor"] + actor = guild.get_member(actor_id) + + reason = site_entry["reason"] or "*None*" + created = time.format_infraction(site_entry["inserted_at"]) + entries.append( + f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" + ) + + entries_string = "\n\n".join(entries) + + active = nomination_object["active"] + + start_date = time.format_infraction(nomination_object["inserted_at"]) + if active: + lines = textwrap.dedent( + f""" + =============== + Status: **Active** + Date: {start_date} + Nomination ID: `{nomination_object["id"]}` + + {entries_string} + =============== + """ + ) + else: + end_date = time.format_infraction(nomination_object["ended_at"]) + lines = textwrap.dedent( + f""" + =============== + Status: Inactive + Date: {start_date} + Nomination ID: `{nomination_object["id"]}` + + {entries_string} + + End date: {end_date} + Unwatch reason: {nomination_object["end_reason"]} + =============== + """ + ) + + return lines.strip() + + +def setup(bot: Bot) -> None: + """Load the TalentPool cog.""" + bot.add_cog(TalentPool(bot)) diff --git a/bot/exts/recruitment/talentpool/talentpool.py b/bot/exts/recruitment/talentpool/talentpool.py deleted file mode 100644 index d75688fa6..000000000 --- a/bot/exts/recruitment/talentpool/talentpool.py +++ /dev/null @@ -1,335 +0,0 @@ -import logging -import textwrap -from collections import ChainMap -from typing import Union - -from discord import Color, Embed, Member, User -from discord.ext.commands import Cog, Context, group, has_any_role - -from bot.api import ResponseCodeError -from bot.bot import Bot -from bot.constants import Channels, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks -from bot.converters import FetchedMember -from bot.exts.moderation.watchchannels._watchchannel import WatchChannel -from bot.pagination import LinePaginator -from bot.utils import time - -REASON_MAX_CHARS = 1000 - -log = logging.getLogger(__name__) - - -class TalentPool(WatchChannel, Cog, name="Talentpool"): - """Relays messages of helper candidates to a watch channel to observe them.""" - - def __init__(self, bot: Bot) -> None: - super().__init__( - bot, - destination=Channels.talent_pool, - webhook_id=Webhooks.talent_pool, - api_endpoint='bot/nominations', - api_default_params={'active': 'true', 'ordering': '-inserted_at'}, - logger=log, - disable_header=True, - ) - - @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) - @has_any_role(*MODERATION_ROLES) - async def nomination_group(self, ctx: Context) -> None: - """Highlights the activity of helper nominees by relaying their messages to the talent pool channel.""" - await ctx.send_help(ctx.command) - - @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) - @has_any_role(*MODERATION_ROLES) - async def watched_command( - self, ctx: Context, oldest_first: bool = False, update_cache: bool = True - ) -> None: - """ - Shows the users that are currently being monitored in the talent pool. - - The optional kwarg `oldest_first` can be used to order the list by oldest nomination. - - The optional kwarg `update_cache` can be used to update the user - cache using the API before listing the users. - """ - await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache) - - @nomination_group.command(name='oldest') - @has_any_role(*MODERATION_ROLES) - async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None: - """ - Shows talent pool monitored users ordered by oldest nomination. - - The optional kwarg `update_cache` can be used to update the user - cache using the API before listing the users. - """ - await ctx.invoke(self.watched_command, oldest_first=True, update_cache=update_cache) - - @nomination_group.command(name='watch', aliases=('w', 'add', 'a'), root_aliases=("nominate",)) - @has_any_role(*STAFF_ROLES) - async def watch_command(self, ctx: Context, user: FetchedMember, *, reason: str = '') -> None: - """ - Relay messages sent by the given `user` to the `#talent-pool` channel. - - A `reason` for adding the user to the talent pool is optional. - If given, it will be displayed in the header when relaying messages of this user to the channel. - """ - if user.bot: - await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I only watch humans.") - return - - if isinstance(user, Member) and any(role.id in STAFF_ROLES for role in user.roles): - await ctx.send(":x: Nominating staff members, eh? Here's a cookie :cookie:") - return - - if not await self.fetch_user_cache(): - await ctx.send(f":x: Failed to update the user cache; can't add {user}") - return - - if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") - return - - # Manual request with `raise_for_status` as False because we want the actual response - session = self.bot.api_client.session - url = self.bot.api_client._url_for(self.api_endpoint) - kwargs = { - 'json': { - 'actor': ctx.author.id, - 'reason': reason, - 'user': user.id - }, - 'raise_for_status': False, - } - async with session.post(url, **kwargs) as resp: - response_data = await resp.json() - - if resp.status == 400: - if response_data.get('user', False): - await ctx.send(":x: The specified user can't be found in the database tables") - elif response_data.get('actor', False): - await ctx.send(":x: You have already nominated this user") - - return - else: - resp.raise_for_status() - - self.watched_users[user.id] = response_data - msg = f":white_check_mark: The nomination for {user} has been added to the talent pool" - - history = await self.bot.api_client.get( - self.api_endpoint, - params={ - "user__id": str(user.id), - "active": "false", - "ordering": "-inserted_at" - } - ) - - if history: - msg += f"\n\n({len(history)} previous nominations in total)" - - await ctx.send(msg) - - @nomination_group.command(name='history', aliases=('info', 'search')) - @has_any_role(*MODERATION_ROLES) - async def history_command(self, ctx: Context, user: FetchedMember) -> None: - """Shows the specified user's nomination history.""" - result = await self.bot.api_client.get( - self.api_endpoint, - params={ - 'user__id': str(user.id), - 'ordering': "-active,-inserted_at" - } - ) - if not result: - await ctx.send(":warning: This user has never been nominated") - return - - embed = Embed( - title=f"Nominations for {user.display_name} `({user.id})`", - color=Color.blue() - ) - lines = [self._nomination_to_string(nomination) for nomination in result] - await LinePaginator.paginate( - lines, - ctx=ctx, - embed=embed, - empty=True, - max_lines=3, - max_size=1000 - ) - - @nomination_group.command(name='unwatch', aliases=('end', ), root_aliases=("unnominate",)) - @has_any_role(*MODERATION_ROLES) - async def unwatch_command(self, ctx: Context, user: FetchedMember, *, reason: str) -> None: - """ - Ends the active nomination of the specified user with the given reason. - - Providing a `reason` is required. - """ - if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") - return - - if await self.unwatch(user.id, reason): - await ctx.send(f":white_check_mark: Messages sent by {user} will no longer be relayed") - else: - await ctx.send(":x: The specified user does not have an active nomination") - - @nomination_group.group(name='edit', aliases=('e',), invoke_without_command=True) - @has_any_role(*MODERATION_ROLES) - async def nomination_edit_group(self, ctx: Context) -> None: - """Commands to edit nominations.""" - await ctx.send_help(ctx.command) - - @nomination_edit_group.command(name='reason') - @has_any_role(*MODERATION_ROLES) - async def edit_reason_command(self, ctx: Context, nomination_id: int, actor: FetchedMember, *, reason: str) -> None: - """Edits the reason of a specific nominator in a specific active nomination.""" - if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") - return - - try: - nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") - except ResponseCodeError as e: - if e.response.status == 404: - self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") - await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") - return - else: - raise - - if not nomination["active"]: - await ctx.send(":x: Can't edit the reason of an inactive nomination.") - return - - if not any(entry["actor"] == actor.id for entry in nomination["entries"]): - await ctx.send(f":x: {actor} doesn't have an entry in this nomination.") - return - - self.log.trace(f"Changing reason for nomination with id {nomination_id} of actor {actor} to {repr(reason)}") - - await self.bot.api_client.patch( - f"{self.api_endpoint}/{nomination_id}", - json={"actor": actor.id, "reason": reason} - ) - await self.fetch_user_cache() # Update cache - await ctx.send(":white_check_mark: Successfully updated nomination reason.") - - @nomination_edit_group.command(name='end_reason') - @has_any_role(*MODERATION_ROLES) - async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: - """Edits the unnominate reason for the nomination with the given `id`.""" - if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") - return - - try: - nomination = await self.bot.api_client.get(f"{self.api_endpoint}/{nomination_id}") - except ResponseCodeError as e: - if e.response.status == 404: - self.log.trace(f"Nomination API 404: Can't find a nomination with id {nomination_id}") - await ctx.send(f":x: Can't find a nomination with id `{nomination_id}`") - return - else: - raise - - if nomination["active"]: - await ctx.send(":x: Can't edit the end reason of an active nomination.") - return - - self.log.trace(f"Changing end reason for nomination with id {nomination_id} to {repr(reason)}") - - await self.bot.api_client.patch( - f"{self.api_endpoint}/{nomination_id}", - json={"end_reason": reason} - ) - await self.fetch_user_cache() # Update cache. - await ctx.send(":white_check_mark: Updated the end reason of the nomination!") - - @Cog.listener() - async def on_member_ban(self, guild: Guild, user: Union[User, Member]) -> None: - """Remove `user` from the talent pool after they are banned.""" - await self.unwatch(user.id, "User was banned.") - - async def unwatch(self, user_id: int, reason: str) -> bool: - """End the active nomination of a user with the given reason and return True on success.""" - active_nomination = await self.bot.api_client.get( - self.api_endpoint, - params=ChainMap( - {"user__id": str(user_id)}, - self.api_default_params, - ) - ) - - if not active_nomination: - log.debug(f"No active nominate exists for {user_id=}") - return False - - log.info(f"Ending nomination: {user_id=} {reason=}") - - nomination = active_nomination[0] - await self.bot.api_client.patch( - f"{self.api_endpoint}/{nomination['id']}", - json={'end_reason': reason, 'active': False} - ) - self._remove_user(user_id) - - return True - - def _nomination_to_string(self, nomination_object: dict) -> str: - """Creates a string representation of a nomination.""" - guild = self.bot.get_guild(Guild.id) - entries = [] - for site_entry in nomination_object["entries"]: - actor_id = site_entry["actor"] - actor = guild.get_member(actor_id) - - reason = site_entry["reason"] or "*None*" - created = time.format_infraction(site_entry["inserted_at"]) - entries.append( - f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" - ) - - entries_string = "\n\n".join(entries) - - active = nomination_object["active"] - - start_date = time.format_infraction(nomination_object["inserted_at"]) - if active: - lines = textwrap.dedent( - f""" - =============== - Status: **Active** - Date: {start_date} - Nomination ID: `{nomination_object["id"]}` - - {entries_string} - =============== - """ - ) - else: - end_date = time.format_infraction(nomination_object["ended_at"]) - lines = textwrap.dedent( - f""" - =============== - Status: Inactive - Date: {start_date} - Nomination ID: `{nomination_object["id"]}` - - {entries_string} - - End date: {end_date} - Unwatch reason: {nomination_object["end_reason"]} - =============== - """ - ) - - return lines.strip() - - -def setup(bot: Bot) -> None: - """Load the TalentPool cog.""" - bot.add_cog(TalentPool(bot)) -- cgit v1.2.3 From f7f38d30cd7c26f9941b77c155ed5876fc2c410a Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Wed, 10 Mar 2021 20:20:17 +0200 Subject: Make talentpool a package and move cog load to __init__.py --- bot/exts/recruitment/talentpool/__init__.py | 8 ++++++++ bot/exts/recruitment/talentpool/_cog.py | 5 ----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 bot/exts/recruitment/talentpool/__init__.py diff --git a/bot/exts/recruitment/talentpool/__init__.py b/bot/exts/recruitment/talentpool/__init__.py new file mode 100644 index 000000000..52d27eb99 --- /dev/null +++ b/bot/exts/recruitment/talentpool/__init__.py @@ -0,0 +1,8 @@ +from bot.bot import Bot + + +def setup(bot: Bot) -> None: + """Load the TalentPool cog.""" + from bot.exts.recruitment.talentpool._cog import TalentPool + + bot.add_cog(TalentPool(bot)) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index d75688fa6..67513f386 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -328,8 +328,3 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) return lines.strip() - - -def setup(bot: Bot) -> None: - """Load the TalentPool cog.""" - bot.add_cog(TalentPool(bot)) -- cgit v1.2.3 From f6b608a977406810d95e4a1dfccbb915bf62268e Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Wed, 10 Mar 2021 20:36:06 +0200 Subject: Add __init__.py to recruitment Make it a package as well so that the talentpool actually loads. --- bot/exts/recruitment/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 bot/exts/recruitment/__init__.py diff --git a/bot/exts/recruitment/__init__.py b/bot/exts/recruitment/__init__.py new file mode 100644 index 000000000..e69de29bb -- cgit v1.2.3 From 4f08f041d03a130012d83c50999a18a39e75dbdc Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Thu, 11 Mar 2021 01:37:40 +0200 Subject: Added an auto-reviewer to the talentpool cog This commit adds the functionality to automatically review a nominee a set number of days after being nominated. This is implemented by subclassing the Scheduler and formatting a review after 30 days. The review contains details of the nominee, their nominations, the number of messages they have and the channels they're most active in, and statistics about their infractions and previous nominations. Lastly, the bot will add three emojis to the review: eyes to mark as seen, a thumbsup, and thumbsdown for the vote itself. The code accounts for the possibility of the review being too long for a single message but splitting it where necessary. --- bot/exts/moderation/watchchannels/_watchchannel.py | 78 ++++-- bot/exts/recruitment/talentpool/_cog.py | 71 +++++- bot/exts/recruitment/talentpool/_review.py | 273 +++++++++++++++++++++ bot/utils/time.py | 8 + 4 files changed, 404 insertions(+), 26 deletions(-) create mode 100644 bot/exts/recruitment/talentpool/_review.py diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index 0793a66af..b121243ce 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -5,9 +5,8 @@ import textwrap from abc import abstractmethod from collections import defaultdict, deque from dataclasses import dataclass -from typing import Optional +from typing import Any, Dict, Optional -import dateutil.parser import discord from discord import Color, DMChannel, Embed, HTTPException, Message, errors from discord.ext.commands import Cog, Context @@ -20,7 +19,7 @@ from bot.exts.filters.webhook_remover import WEBHOOK_URL_RE from bot.exts.moderation.modlog import ModLog from bot.pagination import LinePaginator from bot.utils import CogABCMeta, messages -from bot.utils.time import time_since +from bot.utils.time import get_time_delta log = logging.getLogger(__name__) @@ -136,7 +135,10 @@ class WatchChannel(metaclass=CogABCMeta): if not await self.fetch_user_cache(): await self.modlog.send_log_message( title=f"Warning: Failed to retrieve user cache for the {self.__class__.__name__} watch channel", - text="Could not retrieve the list of watched users from the API and messages will not be relayed.", + text=( + "Could not retrieve the list of watched users from the API. " + "Messages will not be relayed, and reviews not rescheduled." + ), ping_everyone=True, icon_url=Icons.token_removed, colour=Color.red() @@ -280,7 +282,7 @@ class WatchChannel(metaclass=CogABCMeta): actor = actor.display_name if actor else self.watched_users[user_id]['actor'] inserted_at = self.watched_users[user_id]['inserted_at'] - time_delta = self._get_time_delta(inserted_at) + time_delta = get_time_delta(inserted_at) reason = self.watched_users[user_id]['reason'] @@ -308,35 +310,61 @@ class WatchChannel(metaclass=CogABCMeta): The optional kwarg `update_cache` specifies whether the cache should be refreshed by polling the API. """ - if update_cache: - if not await self.fetch_user_cache(): - await ctx.send(f":x: Failed to update {self.__class__.__name__} user cache, serving from cache") - update_cache = False + watched_data = await self.prepare_watched_users_data(ctx, oldest_first, update_cache) - lines = [] - for user_id, user_data in self.watched_users.items(): - inserted_at = user_data['inserted_at'] - time_delta = self._get_time_delta(inserted_at) - lines.append(f"• <@{user_id}> (added {time_delta})") - - if oldest_first: - lines.reverse() + if update_cache and not watched_data["updated"]: + await ctx.send(f":x: Failed to update {self.__class__.__name__} user cache, serving from cache") - lines = lines or ("There's nothing here yet.",) + lines = watched_data["info"].values() or ("There's nothing here yet.",) embed = Embed( - title=f"{self.__class__.__name__} watched users ({'updated' if update_cache else 'cached'})", + title=watched_data["title"], color=Color.blue() ) await LinePaginator.paginate(lines, ctx, embed, empty=False) - @staticmethod - def _get_time_delta(time_string: str) -> str: - """Returns the time in human-readable time delta format.""" - date_time = dateutil.parser.isoparse(time_string).replace(tzinfo=None) - time_delta = time_since(date_time, precision="minutes", max_units=1) + async def prepare_watched_users_data( + self, ctx: Context, oldest_first: bool = False, update_cache: bool = True + ) -> Dict[str, Any]: + """ + Prepare overview information of watched users to list. + + The optional kwarg `oldest_first` orders the list by oldest entry. + + The optional kwarg `update_cache` specifies whether the cache should + be refreshed by polling the API. + + Returns a dictionary with a "title" key for the list's title, and a "info" key with + information about each user. + + The dictionary additionally has an "updated" field which is true if a cache update was + requested and it succeeded. + """ + list_data = {} + if update_cache: + if not await self.fetch_user_cache(): + update_cache = False + list_data["updated"] = update_cache + + watched_iter = self.watched_users.items() + if oldest_first: + watched_iter = reversed(watched_iter) + + list_data["info"] = {} + for user_id, user_data in watched_iter: + member = ctx.guild.get_member(user_id) + line = f"• <@{user_id}>" + if member: + line += f" ({member.name}#{member.discriminator})" + inserted_at = user_data['inserted_at'] + line += f", added {get_time_delta(inserted_at)}" + if not member: # Cross off users who left the server. + line = f"~~{line}~~" + list_data["info"][user_id] = line + + list_data["title"] = f"{self.__class__.__name__} watched users ({'updated' if update_cache else 'cached'})" - return time_delta + return list_data def _remove_user(self, user_id: int) -> None: """Removes a user from a watch channel.""" diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 67513f386..60f5cdf8c 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -1,3 +1,4 @@ + import logging import textwrap from collections import ChainMap @@ -11,6 +12,7 @@ from bot.bot import Bot from bot.constants import Channels, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks from bot.converters import FetchedMember from bot.exts.moderation.watchchannels._watchchannel import WatchChannel +from bot.exts.recruitment.talentpool._review import Reviewer from bot.pagination import LinePaginator from bot.utils import time @@ -33,6 +35,9 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): disable_header=True, ) + self.reviewer = Reviewer(self.__class__.__name__, bot, self) + self.bot.loop.create_task(self.reviewer.reschedule_reviews()) + @group(name='talentpool', aliases=('tp', 'talent', 'nomination', 'n'), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) async def nomination_group(self, ctx: Context) -> None: @@ -54,6 +59,44 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): """ await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache) + async def list_watched_users( + self, ctx: Context, oldest_first: bool = False, update_cache: bool = True + ) -> None: + """ + Gives an overview of the nominated users list. + + It specifies the users' mention, name, how long ago they were nominated, and whether their + review was scheduled or already posted. + + The optional kwarg `oldest_first` orders the list by oldest entry. + + The optional kwarg `update_cache` specifies whether the cache should + be refreshed by polling the API. + """ + # TODO Once the watch channel is removed, this can be done in a smarter way, without splitting and overriding + # the list_watched_users function. + watched_data = await self.prepare_watched_users_data(ctx, oldest_first, update_cache) + + if update_cache and not watched_data["updated"]: + await ctx.send(f":x: Failed to update {self.__class__.__name__} user cache, serving from cache") + + lines = [] + for user_id, line in watched_data["info"].items(): + if self.watched_users[user_id]['reviewed']: + line += " *(reviewed)*" + elif user_id in self.reviewer: + line += " *(scheduled)*" + lines.append(line) + + if not lines: + lines = ("There's nothing here yet.",) + + embed = Embed( + title=watched_data["title"], + color=Color.blue() + ) + await LinePaginator.paginate(lines, ctx, embed, empty=False) + @nomination_group.command(name='oldest') @has_any_role(*MODERATION_ROLES) async def oldest_command(self, ctx: Context, update_cache: bool = True) -> None: @@ -115,7 +158,9 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): resp.raise_for_status() self.watched_users[user.id] = response_data - msg = f":white_check_mark: The nomination for {user} has been added to the talent pool" + + if user.id not in self.reviewer: + self.reviewer.schedule_review(user.id) history = await self.bot.api_client.get( self.api_endpoint, @@ -126,6 +171,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): } ) + msg = f"✅ The nomination for {user} has been added to the talent pool" if history: msg += f"\n\n({len(history)} previous nominations in total)" @@ -249,6 +295,22 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await self.fetch_user_cache() # Update cache. await ctx.send(":white_check_mark: Updated the end reason of the nomination!") + @nomination_group.command(aliases=('mr',)) + async def mark_reviewed(self, ctx: Context, nomination_id: int) -> None: + """Mark a nomination as reviewed and cancel the review task.""" + if not await self.reviewer.mark_reviewed(ctx, nomination_id): + return + await ctx.channel.send(f"✅ The nomination with ID `{nomination_id}` was marked as reviewed.") + + @nomination_group.command(aliases=('review',)) + async def post_review(self, ctx: Context, nomination_id: int) -> None: + """Post the automatic review for the user ahead of time.""" + if not (user_id := await self.reviewer.mark_reviewed(ctx, nomination_id)): + return + + await self.reviewer.post_review(user_id, update_database=False) + await ctx.message.add_reaction("✅") + @Cog.listener() async def on_member_ban(self, guild: Guild, user: Union[User, Member]) -> None: """Remove `user` from the talent pool after they are banned.""" @@ -277,6 +339,8 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) self._remove_user(user_id) + self.reviewer.cancel(user_id) + return True def _nomination_to_string(self, nomination_object: dict) -> str: @@ -328,3 +392,8 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): ) return lines.strip() + + def cog_unload(self) -> None: + """Cancels all review tasks on cog unload.""" + super().cog_unload() + self.reviewer.cancel_all() diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py new file mode 100644 index 000000000..64a1c6226 --- /dev/null +++ b/bot/exts/recruitment/talentpool/_review.py @@ -0,0 +1,273 @@ +import asyncio +import logging +import textwrap +import typing +from collections import Counter +from datetime import datetime, timedelta +from typing import List, Optional + +from dateutil.parser import isoparse +from dateutil.relativedelta import relativedelta +from discord import Member, Message, TextChannel +from discord.ext.commands import Context + +from bot.api import ResponseCodeError +from bot.bot import Bot +from bot.constants import Channels, Guild, Roles +from bot.utils.scheduling import Scheduler +from bot.utils.time import get_time_delta, humanize_delta, time_since + +if typing.TYPE_CHECKING: + from bot.exts.recruitment.talentpool._cog import TalentPool + +log = logging.getLogger(__name__) + +# Maximum amount of days before an automatic review is posted. +MAX_DAYS_IN_POOL = 30 + +# Maximum amount of characters allowed in a message +MAX_MESSAGE_SIZE = 2000 + + +class Reviewer(Scheduler): + """Schedules, formats, and publishes reviews of helper nominees.""" + + def __init__(self, name: str, bot: Bot, pool: 'TalentPool'): + super().__init__(name) + self.bot = bot + self._pool = pool + + async def reschedule_reviews(self) -> None: + """Reschedule all active nominations to be reviewed at the appropriate time.""" + log.trace("Rescheduling reviews") + await self.bot.wait_until_guild_available() + # TODO Once the watch channel is removed, this can be done in a smarter way, e.g create a sync function. + await self._pool.fetch_user_cache() + + for user_id, user_data in self._pool.watched_users.items(): + if not user_data["reviewed"]: + self.schedule_review(user_id) + + def schedule_review(self, user_id: int) -> None: + """Schedules a single user for review.""" + log.trace(f"Scheduling review of user with ID {user_id}") + + user_data = self._pool.watched_users[user_id] + inserted_at = isoparse(user_data['inserted_at']).replace(tzinfo=None) + review_at = inserted_at + timedelta(days=MAX_DAYS_IN_POOL) + + self.schedule_at(review_at, user_id, self.post_review(user_id, update_database=True)) + + async def post_review(self, user_id: int, update_database: bool) -> None: + """Format a generic review of a user and post it to the mod announcements channel.""" + log.trace(f"Posting the review of {user_id}") + + nomination = self._pool.watched_users[user_id] + guild = self.bot.get_guild(Guild.id) + channel = guild.get_channel(Channels.mod_announcements) + member = guild.get_member(user_id) + if not member: + channel.send(f"I tried to review the user with ID `{user_id}`, but they don't appear to be on the server 😔") + return + + if update_database: + await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True}) + + opening = f"<@&{Roles.moderators}> <@&{Roles.admins}>\n{member.mention} ({member}) for Helper!" + + current_nominations = "\n\n".join( + f"**<@{entry['actor']}>:** {entry['reason']}" for entry in nomination['entries'] + ) + current_nominations = f"**Nominated by:**\n{current_nominations}" + + review_body = await self._construct_review_body(member) + + vote_request = "*Refer to their nomination and infraction histories for further details*.\n" + vote_request += "*Please react 👀 if you've seen this post. Then react 👍 for approval, or 👎 for disapproval*." + + review = "\n\n".join(part for part in (opening, current_nominations, review_body, vote_request)) + + message = (await self._bulk_send(channel, review))[-1] + for reaction in ("👀", "👍", "👎"): + await message.add_reaction(reaction) + + async def _construct_review_body(self, member: Member) -> str: + """Formats the body of the nomination, with details of activity, infractions, and previous nominations.""" + activity = await self._activity_review(member) + infractions = await self._infractions_review(member) + prev_nominations = await self._previous_nominations_review(member) + + body = f"{activity}\n\n{infractions}" + if prev_nominations: + body += f"\n\n{prev_nominations}" + return body + + async def _activity_review(self, member: Member) -> str: + """ + Format the activity of the nominee. + + Adds details on how long they've been on the server, their total message count, + and the channels they're the most active in. + """ + log.trace(f"Fetching the metricity data for {member.id}'s review") + try: + user_activity = await self.bot.api_client.get(f"bot/users/{member.id}/metricity_review_data") + except ResponseCodeError as e: + if e.status == 404: + messages = "no" + channels = "" + else: + raise + else: + messages = user_activity["total_messages"] + # Making this part flexible to the amount of expected and returned channels. + first_channel = user_activity["top_channel_activity"][0] + channels = f", with {first_channel[1]} messages in {first_channel[0]}" + + if len(user_activity["top_channel_activity"]) > 1: + channels += ", " + ", ".join( + f"{count} in {channel}" for channel, count in user_activity["top_channel_activity"][1: -1] + ) + last_channel = user_activity["top_channel_activity"][-1] + channels += f", and {last_channel[1]} in {last_channel[0]}" + + time_on_server = humanize_delta(relativedelta(datetime.utcnow(), member.joined_at), max_units=2) + review = f"{member.name} has been on the server for **{time_on_server}**" + review += f" and has **{messages} messages**{channels}." + + return review + + async def _infractions_review(self, member: Member) -> str: + """ + Formats the review of the nominee's infractions, if any. + + The infractions are listed by type and amount, and it is stated how long ago the last one was issued. + """ + log.trace(f"Fetching the infraction data for {member.id}'s review") + infraction_list = await self.bot.api_client.get( + 'bot/infractions/expanded', + params={'user__id': str(member.id), 'ordering': '-inserted_at'} + ) + + if not infraction_list: + return "They have no infractions." + + # Count the amount of each type of infraction. + infr_stats = list(Counter(infr["type"] for infr in infraction_list).items()) + + # Format into a sentence. + infractions = ", ".join( + f"{count} {self._format_infr_name(infr_type, count)}" + for infr_type, count in infr_stats[:-1] + ) + if len(infr_stats) > 1: + last_infr, last_count = infr_stats[-1] + infractions += f", and {last_count} {self._format_infr_name(last_infr, last_count)}" + + infractions = f"**{infractions}**" + + # Show when the last one was issued. + if len(infraction_list) == 1: + infractions += ", issued " + else: + infractions += ", with the last infraction issued " + + # Infractions were ordered by time since insertion descending. + infractions += get_time_delta(infraction_list[0]['inserted_at']) + + return f"They have {infractions}." + + @staticmethod + def _format_infr_name(infr_type: str, count: int) -> str: + """ + Format the infraction type in a way readable in a sentence. + + Underscores are replaced with spaces, as well as *attempting* to show the appropriate plural form if necessary. + This function by no means covers all rules of grammar. + """ + formatted = infr_type.replace("_", " ") + if count > 1: + if infr_type.endswith(('ch', 'sh')): + formatted += "e" + formatted += "s" + + return formatted + + async def _previous_nominations_review(self, member: Member) -> Optional[str]: + """ + Formats the review of the nominee's previous nominations. + + The number of previous nominations and unnominations are shown, as well as the reason the last one ended. + """ + log.trace(f"Fetching the nomination history data for {member.id}'s review") + history = await self.bot.api_client.get( + self._pool.api_endpoint, + params={ + "user__id": str(member.id), + "active": "false", + "ordering": "-inserted_at" + } + ) + + if not history: + return + + num_entries = sum(len(nomination["entries"]) for nomination in history) + + nomination_times = f"{num_entries} times" if num_entries > 1 else "once" + rejection_times = f"{len(history)} times" if len(history) > 1 else "once" + review = f"They were nominated **{nomination_times}** before" + review += f", but their nomination was called off **{rejection_times}**." + + end_time = time_since(isoparse(history[0]['ended_at']).replace(tzinfo=None), max_units=2) + review += f"\nThe last one ended {end_time} with the reason: {history[0]['end_reason']}" + + return review + + @staticmethod + async def _bulk_send(channel: TextChannel, text: str) -> List[Message]: + """ + Split a text into several if necessary, and post them to the channel. + + Returns the resulting message objects. + """ + messages = textwrap.wrap(text, width=MAX_MESSAGE_SIZE, replace_whitespace=False) + + results = [] + for message in messages: + await asyncio.sleep(1) + results.append(await channel.send(message)) + + return results + + async def mark_reviewed(self, ctx: Context, nomination_id: int) -> Optional[int]: + """ + Mark an active nomination as reviewed, updating the database and canceling the review task. + + On success, returns the user ID. + """ + log.trace(f"Updating nomination #{nomination_id} as review") + try: + nomination = await self.bot.api_client.get(f"{self._pool.api_endpoint}/{nomination_id}") + except ResponseCodeError as e: + if e.response.status == 404: + self.log.trace(f"Nomination API 404: Can't find nomination with id {nomination_id}") + await ctx.send(f"❌ Can't find a nomination with id `{nomination_id}`") + return None + else: + raise + + if nomination["reviewed"]: + await ctx.send("❌ This nomination was already reviewed, but here's a cookie 🍪") + return None + elif not nomination["active"]: + await ctx.send("❌ This nomination is inactive") + return None + + await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True}) + if nomination["user"] in self: + self.cancel(nomination["user"]) + + await self._pool.fetch_user_cache() + + return nomination["user"] diff --git a/bot/utils/time.py b/bot/utils/time.py index f862e40f7..466f0adc2 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -85,6 +85,14 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: return humanized +def get_time_delta(time_string: str) -> str: + """Returns the time in human-readable time delta format.""" + date_time = dateutil.parser.isoparse(time_string).replace(tzinfo=None) + time_delta = time_since(date_time, precision="minutes", max_units=1) + + return time_delta + + def parse_duration_string(duration: str) -> Optional[relativedelta]: """ Converts a `duration` string to a relativedelta object. -- cgit v1.2.3 From 0eb8059a0ba6bb6bce464b4b3afb7847aa3bf098 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Thu, 11 Mar 2021 02:59:13 +0200 Subject: Limit new commands to mods+ --- bot/exts/recruitment/talentpool/_cog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 60f5cdf8c..070a4fd83 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -296,6 +296,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await ctx.send(":white_check_mark: Updated the end reason of the nomination!") @nomination_group.command(aliases=('mr',)) + @has_any_role(*MODERATION_ROLES) async def mark_reviewed(self, ctx: Context, nomination_id: int) -> None: """Mark a nomination as reviewed and cancel the review task.""" if not await self.reviewer.mark_reviewed(ctx, nomination_id): @@ -303,6 +304,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await ctx.channel.send(f"✅ The nomination with ID `{nomination_id}` was marked as reviewed.") @nomination_group.command(aliases=('review',)) + @has_any_role(*MODERATION_ROLES) async def post_review(self, ctx: Context, nomination_id: int) -> None: """Post the automatic review for the user ahead of time.""" if not (user_id := await self.reviewer.mark_reviewed(ctx, nomination_id)): -- cgit v1.2.3 From 608f755deead9f180d8c714b69d82c606dba931a Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Thu, 11 Mar 2021 22:50:02 +0200 Subject: The 'seen vote' emoji is now a random ducky. --- bot/exts/recruitment/talentpool/_review.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 64a1c6226..adab1a907 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -1,14 +1,15 @@ import asyncio import logging +import random import textwrap import typing from collections import Counter from datetime import datetime, timedelta -from typing import List, Optional +from typing import List, Optional, Union from dateutil.parser import isoparse from dateutil.relativedelta import relativedelta -from discord import Member, Message, TextChannel +from discord import Emoji, Member, Message, TextChannel from discord.ext.commands import Context from bot.api import ResponseCodeError @@ -82,13 +83,15 @@ class Reviewer(Scheduler): review_body = await self._construct_review_body(member) + seen_emoji = self._random_ducky(guild) vote_request = "*Refer to their nomination and infraction histories for further details*.\n" - vote_request += "*Please react 👀 if you've seen this post. Then react 👍 for approval, or 👎 for disapproval*." + vote_request += f"*Please react {seen_emoji} if you've seen this post." + vote_request += " Then react 👍 for approval, or 👎 for disapproval*." review = "\n\n".join(part for part in (opening, current_nominations, review_body, vote_request)) message = (await self._bulk_send(channel, review))[-1] - for reaction in ("👀", "👍", "👎"): + for reaction in (seen_emoji, "👍", "👎"): await message.add_reaction(reaction) async def _construct_review_body(self, member: Member) -> str: @@ -224,6 +227,14 @@ class Reviewer(Scheduler): return review + @staticmethod + def _random_ducky(guild: Guild) -> Union[Emoji, str]: + """Picks a random ducky emoji to be used to mark the vote as seen. If no duckies found returns 👀.""" + duckies = [emoji for emoji in guild.emojis if emoji.name.startswith("ducky")] + if not duckies: + return "👀" + return random.choice(duckies) + @staticmethod async def _bulk_send(channel: TextChannel, text: str) -> List[Message]: """ -- cgit v1.2.3 From 326cd6dccee276da9b6deee827cb893615be352b Mon Sep 17 00:00:00 2001 From: kwzrd Date: Thu, 11 Mar 2021 23:57:36 +0100 Subject: Compose: read GitHub API key from '.env' --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 0002d1d56..f9a29388d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,3 +62,4 @@ services: BOT_API_KEY: badbot13m0n8f570f942013fc818f234916ca531 REDDIT_CLIENT_ID: ${REDDIT_CLIENT_ID} REDDIT_SECRET: ${REDDIT_SECRET} + GITHUB_API_KEY: ${GITHUB_API_KEY} -- cgit v1.2.3 From a8c0da00248fa3dc3100a55e47b7c2df5952e0a4 Mon Sep 17 00:00:00 2001 From: kwzrd Date: Fri, 12 Mar 2021 00:56:25 +0100 Subject: Compose: read all environment variables from '.env' --- docker-compose.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f9a29388d..8afdd6ef1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,9 +57,7 @@ services: - web - redis - snekbox + env_file: + - .env environment: - BOT_TOKEN: ${BOT_TOKEN} BOT_API_KEY: badbot13m0n8f570f942013fc818f234916ca531 - REDDIT_CLIENT_ID: ${REDDIT_CLIENT_ID} - REDDIT_SECRET: ${REDDIT_SECRET} - GITHUB_API_KEY: ${GITHUB_API_KEY} -- cgit v1.2.3 From 92bfdd3e4aab061d62387ba2abc413d7e803641b Mon Sep 17 00:00:00 2001 From: xithrius Date: Mon, 8 Mar 2021 01:05:01 -0800 Subject: Remove invoked command and message after failure. --- bot/exts/info/pypi.py | 62 +++++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 8fe249c8a..10029aa73 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -8,7 +8,7 @@ from discord.ext.commands import Cog, Context, command from discord.utils import escape_markdown from bot.bot import Bot -from bot.constants import Colours, NEGATIVE_REPLIES +from bot.constants import Colours, NEGATIVE_REPLIES, RedirectOutput URL = "https://pypi.org/pypi/{package}/json" FIELDS = ("author", "requires_python", "summary", "license") @@ -17,6 +17,7 @@ PYPI_ICON = "https://cdn.discordapp.com/emojis/766274397257334814.png" PYPI_COLOURS = itertools.cycle((Colours.yellow, Colours.blue, Colours.white)) ILLEGAL_CHARACTERS = re.compile(r"[^a-zA-Z0-9-.]+") +INVALID_INPUT_DELETE_DELAY = RedirectOutput.delete_delay log = logging.getLogger(__name__) @@ -36,42 +37,49 @@ class PyPi(Cog): ) embed.set_thumbnail(url=PYPI_ICON) + error = True + if (character := re.search(ILLEGAL_CHARACTERS, package)) is not None: embed.description = f"Illegal character passed into command: '{escape_markdown(character.group(0))}'" - await ctx.send(embed=embed) - return - async with self.bot.http_session.get(URL.format(package=package)) as response: - if response.status == 404: - embed.description = "Package could not be found." + else: + async with self.bot.http_session.get(URL.format(package=package)) as response: + if response.status == 404: + embed.description = "Package could not be found." - elif response.status == 200 and response.content_type == "application/json": - response_json = await response.json() - info = response_json["info"] + elif response.status == 200 and response.content_type == "application/json": + response_json = await response.json() + info = response_json["info"] - embed.title = f"{info['name']} v{info['version']}" - embed.url = info['package_url'] - embed.colour = next(PYPI_COLOURS) + embed.title = f"{info['name']} v{info['version']}" + embed.url = info['package_url'] + embed.colour = next(PYPI_COLOURS) - for field in FIELDS: - field_data = info[field] + for field in FIELDS: + field_data = info[field] - # Field could be completely empty, in some cases can be a string with whitespaces, or None. - if field_data and not field_data.isspace(): - if '\n' in field_data and field == "license": - field_data = field_data.split('\n')[0] + # Field could be completely empty, in some cases can be a string with whitespaces, or None. + if field_data and not field_data.isspace(): + if '\n' in field_data and field == "license": + field_data = field_data.split('\n')[0] - embed.add_field( - name=field.replace("_", " ").title(), - value=escape_markdown(field_data), - inline=False, - ) + embed.add_field( + name=field.replace("_", " ").title(), + value=escape_markdown(field_data), + inline=False, + ) - else: - embed.description = "There was an error when fetching your PyPi package." - log.trace(f"Error when fetching PyPi package: {response.status}.") + error = False - await ctx.send(embed=embed) + else: + embed.description = "There was an error when fetching your PyPi package." + log.trace(f"Error when fetching PyPi package: {response.status}.") + + if error: + await ctx.send(embed=embed, delete_after=INVALID_INPUT_DELETE_DELAY) + await ctx.message.delete(delay=INVALID_INPUT_DELETE_DELAY) + else: + await ctx.send(embed=embed) def setup(bot: Bot) -> None: -- cgit v1.2.3 From de0bc6ea58a2766d9637af80e703e11291e424e1 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 12 Mar 2021 14:14:12 +0200 Subject: Reviewer no longer subclasses Scheduler It didn't make much sense for the Reviewer to subclasses Scheduler. The Scheduler has methods that don't make sense to use on the Reviewer directly. There is now a Scheduler object as an attribute of the Reviewer. Interacting with it is done by adding __contains__, cancel, and cancel_all methods. --- bot/exts/recruitment/talentpool/_review.py | 36 +++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index adab1a907..beb4c130f 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -30,13 +30,17 @@ MAX_DAYS_IN_POOL = 30 MAX_MESSAGE_SIZE = 2000 -class Reviewer(Scheduler): +class Reviewer: """Schedules, formats, and publishes reviews of helper nominees.""" def __init__(self, name: str, bot: Bot, pool: 'TalentPool'): - super().__init__(name) self.bot = bot self._pool = pool + self._review_scheduler = Scheduler(name) + + def __contains__(self, user_id: int) -> bool: + """Return True if the user with ID user_id is scheduled for review, False otherwise.""" + return user_id in self._review_scheduler async def reschedule_reviews(self) -> None: """Reschedule all active nominations to be reviewed at the appropriate time.""" @@ -57,13 +61,17 @@ class Reviewer(Scheduler): inserted_at = isoparse(user_data['inserted_at']).replace(tzinfo=None) review_at = inserted_at + timedelta(days=MAX_DAYS_IN_POOL) - self.schedule_at(review_at, user_id, self.post_review(user_id, update_database=True)) + self._review_scheduler.schedule_at(review_at, user_id, self.post_review(user_id, update_database=True)) async def post_review(self, user_id: int, update_database: bool) -> None: """Format a generic review of a user and post it to the mod announcements channel.""" log.trace(f"Posting the review of {user_id}") nomination = self._pool.watched_users[user_id] + if not nomination: + log.trace(f"There doesn't appear to be an active nomination for {user_id}") + return + guild = self.bot.get_guild(Guild.id) channel = guild.get_channel(Channels.mod_announcements) member = guild.get_member(user_id) @@ -276,9 +284,27 @@ class Reviewer(Scheduler): return None await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True}) - if nomination["user"] in self: - self.cancel(nomination["user"]) + if nomination["user"] in self._review_scheduler: + self._review_scheduler.cancel(nomination["user"]) await self._pool.fetch_user_cache() return nomination["user"] + + def cancel(self, user_id: int) -> None: + """ + Cancels the review of the nominee with ID user_id. + + It's important to note that this applies only until reschedule_reviews is called again. + To permenantly cancel someone's review, either remove them from the pool, or use mark_reviewed. + """ + self._review_scheduler.cancel(user_id) + + def cancel_all(self) -> None: + """ + Cancels all reviews. + + It's important to note that this applies only until reschedule_reviews is called again. + To permenantly cancel someone's review, either remove them from the pool, or use mark_reviewed. + """ + self._review_scheduler.cancel_all() -- cgit v1.2.3 From 4f17ba526995927fa3b1fb8e925179ab61e26265 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 12 Mar 2021 15:16:43 +0200 Subject: Improve string building for long lines --- bot/exts/recruitment/talentpool/_review.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index beb4c130f..56b51925e 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -92,9 +92,11 @@ class Reviewer: review_body = await self._construct_review_body(member) seen_emoji = self._random_ducky(guild) - vote_request = "*Refer to their nomination and infraction histories for further details*.\n" - vote_request += f"*Please react {seen_emoji} if you've seen this post." - vote_request += " Then react 👍 for approval, or 👎 for disapproval*." + vote_request = ( + "*Refer to their nomination and infraction histories for further details*.\n" + f"*Please react {seen_emoji} if you've seen this post." + " Then react 👍 for approval, or 👎 for disapproval*." + ) review = "\n\n".join(part for part in (opening, current_nominations, review_body, vote_request)) @@ -143,8 +145,10 @@ class Reviewer: channels += f", and {last_channel[1]} in {last_channel[0]}" time_on_server = humanize_delta(relativedelta(datetime.utcnow(), member.joined_at), max_units=2) - review = f"{member.name} has been on the server for **{time_on_server}**" - review += f" and has **{messages} messages**{channels}." + review = ( + f"{member.name} has been on the server for **{time_on_server}**" + f" and has **{messages} messages**{channels}." + ) return review @@ -227,11 +231,13 @@ class Reviewer: nomination_times = f"{num_entries} times" if num_entries > 1 else "once" rejection_times = f"{len(history)} times" if len(history) > 1 else "once" - review = f"They were nominated **{nomination_times}** before" - review += f", but their nomination was called off **{rejection_times}**." - end_time = time_since(isoparse(history[0]['ended_at']).replace(tzinfo=None), max_units=2) - review += f"\nThe last one ended {end_time} with the reason: {history[0]['end_reason']}" + + review = ( + f"They were nominated **{nomination_times}** before" + f", but their nomination was called off **{rejection_times}**." + f"\nThe last one ended {end_time} with the reason: {history[0]['end_reason']}" + ) return review -- cgit v1.2.3 From e82931be287d956237ad2e0562e46492f4f5b839 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 12 Mar 2021 14:51:46 +0100 Subject: Fix typo in the token remover --- bot/exts/filters/webhook_remover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/webhook_remover.py b/bot/exts/filters/webhook_remover.py index 08fe94055..f11fc8912 100644 --- a/bot/exts/filters/webhook_remover.py +++ b/bot/exts/filters/webhook_remover.py @@ -14,7 +14,7 @@ WEBHOOK_URL_RE = re.compile(r"((?:https?://)?discord(?:app)?\.com/api/webhooks/\ ALERT_MESSAGE_TEMPLATE = ( "{user}, looks like you posted a Discord webhook URL. Therefore, your " "message has been removed. Your webhook may have been **compromised** so " - "please re-create the webhook **immediately**. If you believe this was " + "please re-create the webhook **immediately**. If you believe this was a " "mistake, please let us know." ) -- cgit v1.2.3 From 1b1e7adaca4b116a69db06955ab2a3edb222ef52 Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 12 Mar 2021 11:39:12 -0800 Subject: Added '_' to allowed chars, shortened embed. --- bot/exts/info/pypi.py | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 10029aa73..2e42e7d6b 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -11,12 +11,11 @@ from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES, RedirectOutput URL = "https://pypi.org/pypi/{package}/json" -FIELDS = ("author", "requires_python", "summary", "license") PYPI_ICON = "https://cdn.discordapp.com/emojis/766274397257334814.png" PYPI_COLOURS = itertools.cycle((Colours.yellow, Colours.blue, Colours.white)) -ILLEGAL_CHARACTERS = re.compile(r"[^a-zA-Z0-9-.]+") +ILLEGAL_CHARACTERS = re.compile(r"[^-_.a-zA-Z0-9]+") INVALID_INPUT_DELETE_DELAY = RedirectOutput.delete_delay log = logging.getLogger(__name__) @@ -31,16 +30,13 @@ class PyPi(Cog): @command(name="pypi", aliases=("package", "pack")) async def get_package_info(self, ctx: Context, package: str) -> None: """Provide information about a specific package from PyPI.""" - embed = Embed( - title=random.choice(NEGATIVE_REPLIES), - colour=Colours.soft_red - ) + embed = Embed(title=random.choice(NEGATIVE_REPLIES), colour=Colours.soft_red) embed.set_thumbnail(url=PYPI_ICON) error = True - if (character := re.search(ILLEGAL_CHARACTERS, package)) is not None: - embed.description = f"Illegal character passed into command: '{escape_markdown(character.group(0))}'" + if characters := re.search(ILLEGAL_CHARACTERS, package): + embed.description = f"Illegal character(s) passed into command: '{escape_markdown(characters.group(0))}'" else: async with self.bot.http_session.get(URL.format(package=package)) as response: @@ -52,22 +48,17 @@ class PyPi(Cog): info = response_json["info"] embed.title = f"{info['name']} v{info['version']}" - embed.url = info['package_url'] - embed.colour = next(PYPI_COLOURS) - for field in FIELDS: - field_data = info[field] + embed.url = info["package_url"] + embed.colour = next(PYPI_COLOURS) - # Field could be completely empty, in some cases can be a string with whitespaces, or None. - if field_data and not field_data.isspace(): - if '\n' in field_data and field == "license": - field_data = field_data.split('\n')[0] + summary = escape_markdown(info["summary"]) - embed.add_field( - name=field.replace("_", " ").title(), - value=escape_markdown(field_data), - inline=False, - ) + # Summary could be completely empty, or just whitespace. + if summary and not summary.isspace(): + embed.description = summary + else: + embed.description = "No summary provided." error = False -- cgit v1.2.3 From 7bc390ed20bda22cf5a2b455be6d4b15eedf47c0 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 13 Mar 2021 11:04:25 +0000 Subject: Update help channel names from chemical elements to fruit * Update and rename elements.json to fruits.json * Update _name.py * Update _cog.py --- bot/exts/help_channels/_cog.py | 2 +- bot/exts/help_channels/_name.py | 12 ++-- bot/resources/elements.json | 119 ---------------------------------------- bot/resources/foods.json | 52 ++++++++++++++++++ 4 files changed, 59 insertions(+), 126 deletions(-) delete mode 100644 bot/resources/elements.json create mode 100644 bot/resources/foods.json diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 6abf99810..1c730dce9 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -54,7 +54,7 @@ class HelpChannels(commands.Cog): * Contains channels which aren't in use * Channels are used to refill the Available category - Help channels are named after the chemical elements in `bot/resources/elements.json`. + Help channels are named after the foods in `bot/resources/foods.json`. """ def __init__(self, bot: Bot): diff --git a/bot/exts/help_channels/_name.py b/bot/exts/help_channels/_name.py index 728234b1e..061f855ae 100644 --- a/bot/exts/help_channels/_name.py +++ b/bot/exts/help_channels/_name.py @@ -14,11 +14,11 @@ log = logging.getLogger(__name__) def create_name_queue(*categories: discord.CategoryChannel) -> deque: """ - Return a queue of element names to use for creating new channels. + Return a queue of food names to use for creating new channels. Skip names that are already in use by channels in `categories`. """ - log.trace("Creating the chemical element name queue.") + log.trace("Creating the food name queue.") used_names = _get_used_names(*categories) @@ -31,7 +31,7 @@ def create_name_queue(*categories: discord.CategoryChannel) -> deque: def _get_names() -> t.List[str]: """ - Return a truncated list of prefixed element names. + Return a truncated list of prefixed food names. The amount of names is configured with `HelpChannels.max_total_channels`. The prefix is configured with `HelpChannels.name_prefix`. @@ -39,10 +39,10 @@ def _get_names() -> t.List[str]: count = constants.HelpChannels.max_total_channels prefix = constants.HelpChannels.name_prefix - log.trace(f"Getting the first {count} element names from JSON.") + log.trace(f"Getting the first {count} food names from JSON.") - with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: - all_names = json.load(elements_file) + with Path("bot/resources/foods.json").open(encoding="utf-8") as foods_file: + all_names = json.load(foods_file) if prefix: return [prefix + name for name in all_names[:count]] diff --git a/bot/resources/elements.json b/bot/resources/elements.json deleted file mode 100644 index a3ac5b99f..000000000 --- a/bot/resources/elements.json +++ /dev/null @@ -1,119 +0,0 @@ -[ - "hydrogen", - "helium", - "lithium", - "beryllium", - "boron", - "carbon", - "nitrogen", - "oxygen", - "fluorine", - "neon", - "sodium", - "magnesium", - "aluminium", - "silicon", - "phosphorus", - "sulfur", - "chlorine", - "argon", - "potassium", - "calcium", - "scandium", - "titanium", - "vanadium", - "chromium", - "manganese", - "iron", - "cobalt", - "nickel", - "copper", - "zinc", - "gallium", - "germanium", - "arsenic", - "bromine", - "krypton", - "rubidium", - "strontium", - "yttrium", - "zirconium", - "niobium", - "molybdenum", - "technetium", - "ruthenium", - "rhodium", - "palladium", - "silver", - "cadmium", - "indium", - "tin", - "antimony", - "tellurium", - "iodine", - "xenon", - "caesium", - "barium", - "lanthanum", - "cerium", - "praseodymium", - "neodymium", - "promethium", - "samarium", - "europium", - "gadolinium", - "terbium", - "dysprosium", - "holmium", - "erbium", - "thulium", - "ytterbium", - "lutetium", - "hafnium", - "tantalum", - "tungsten", - "rhenium", - "osmium", - "iridium", - "platinum", - "gold", - "mercury", - "thallium", - "lead", - "bismuth", - "polonium", - "astatine", - "radon", - "francium", - "radium", - "actinium", - "thorium", - "protactinium", - "uranium", - "neptunium", - "plutonium", - "americium", - "curium", - "berkelium", - "californium", - "einsteinium", - "fermium", - "mendelevium", - "nobelium", - "lawrencium", - "rutherfordium", - "dubnium", - "seaborgium", - "bohrium", - "hassium", - "meitnerium", - "darmstadtium", - "roentgenium", - "copernicium", - "nihonium", - "flerovium", - "moscovium", - "livermorium", - "tennessine", - "oganesson" -] diff --git a/bot/resources/foods.json b/bot/resources/foods.json new file mode 100644 index 000000000..61d9ea98f --- /dev/null +++ b/bot/resources/foods.json @@ -0,0 +1,52 @@ +[ + "apple", + "avocado", + "bagel", + "banana", + "bread", + "broccoli", + "burrito", + "cake", + "candy", + "carrot", + "cheese", + "cherries", + "chestnut", + "chili", + "chocolate", + "coconut", + "coffee", + "cookie", + "corn", + "croissant", + "cupcake", + "donut", + "dumpling", + "falafel", + "grapes", + "honey", + "kiwi", + "lemon", + "lollipop", + "mango", + "mushroom", + "orange", + "pancakes", + "peanut", + "pear", + "pie", + "pineapple", + "popcorn", + "potato", + "pretzel", + "ramen", + "rice", + "salad", + "spaghetti", + "stew", + "strawberry", + "sushi", + "taco", + "tomato", + "watermelon" +] -- cgit v1.2.3 From 4b5af57b4ed4eac18bf3c368f99e848e10a33cab Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 13 Mar 2021 16:20:47 +0200 Subject: Use log instead of erroneous self.log --- bot/exts/recruitment/talentpool/_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 56b51925e..b84499d98 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -276,7 +276,7 @@ class Reviewer: nomination = await self.bot.api_client.get(f"{self._pool.api_endpoint}/{nomination_id}") except ResponseCodeError as e: if e.response.status == 404: - self.log.trace(f"Nomination API 404: Can't find nomination with id {nomination_id}") + log.trace(f"Nomination API 404: Can't find nomination with id {nomination_id}") await ctx.send(f"❌ Can't find a nomination with id `{nomination_id}`") return None else: -- cgit v1.2.3 From 4324b3f6ac80bbcbd2eef80303bf7caf1dfa8cca Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 13 Mar 2021 16:27:28 +0200 Subject: Apply requested grammar and style changes. --- bot/exts/recruitment/talentpool/_cog.py | 11 ++++++++--- bot/exts/recruitment/talentpool/_review.py | 10 +++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 070a4fd83..7b21dcd53 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -1,4 +1,3 @@ - import logging import textwrap from collections import ChainMap @@ -47,7 +46,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): @nomination_group.command(name='watched', aliases=('all', 'list'), root_aliases=("nominees",)) @has_any_role(*MODERATION_ROLES) async def watched_command( - self, ctx: Context, oldest_first: bool = False, update_cache: bool = True + self, + ctx: Context, + oldest_first: bool = False, + update_cache: bool = True ) -> None: """ Shows the users that are currently being monitored in the talent pool. @@ -60,7 +62,10 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): await self.list_watched_users(ctx, oldest_first=oldest_first, update_cache=update_cache) async def list_watched_users( - self, ctx: Context, oldest_first: bool = False, update_cache: bool = True + self, + ctx: Context, + oldest_first: bool = False, + update_cache: bool = True ) -> None: """ Gives an overview of the nominated users list. diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index b84499d98..682a32918 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -278,16 +278,16 @@ class Reviewer: if e.response.status == 404: log.trace(f"Nomination API 404: Can't find nomination with id {nomination_id}") await ctx.send(f"❌ Can't find a nomination with id `{nomination_id}`") - return None + return else: raise if nomination["reviewed"]: await ctx.send("❌ This nomination was already reviewed, but here's a cookie 🍪") - return None + return elif not nomination["active"]: await ctx.send("❌ This nomination is inactive") - return None + return await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True}) if nomination["user"] in self._review_scheduler: @@ -302,7 +302,7 @@ class Reviewer: Cancels the review of the nominee with ID user_id. It's important to note that this applies only until reschedule_reviews is called again. - To permenantly cancel someone's review, either remove them from the pool, or use mark_reviewed. + To permanently cancel someone's review, either remove them from the pool, or use mark_reviewed. """ self._review_scheduler.cancel(user_id) @@ -311,6 +311,6 @@ class Reviewer: Cancels all reviews. It's important to note that this applies only until reschedule_reviews is called again. - To permenantly cancel someone's review, either remove them from the pool, or use mark_reviewed. + To permanently cancel someone's review, either remove them from the pool, or use mark_reviewed. """ self._review_scheduler.cancel_all() -- cgit v1.2.3 From a394c42f32f07c2932e641a48a51e16f949f36ee Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 13 Mar 2021 19:51:04 +0000 Subject: master => main --- .github/workflows/build.yml | 2 +- .github/workflows/deploy.yml | 2 +- .github/workflows/lint-test.yml | 2 +- .github/workflows/sentry_release.yml | 4 ++-- CONTRIBUTING.md | 6 +++--- README.md | 14 +++++++------- bot/exts/backend/branding/_constants.py | 2 +- bot/exts/backend/logging.py | 2 +- bot/exts/info/source.py | 2 +- config-default.yml | 8 ++++---- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c97e8784..e6826e09b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: workflow_run: workflows: ["Lint & Test"] branches: - - master + - main types: - completed diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0caf02308..8b809b777 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,7 +4,7 @@ on: workflow_run: workflows: ["Build"] branches: - - master + - main types: - completed diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index 6fa8e8333..95bed2e14 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -3,7 +3,7 @@ name: Lint & Test on: push: branches: - - master + - main pull_request: diff --git a/.github/workflows/sentry_release.yml b/.github/workflows/sentry_release.yml index b8d92e90a..f6a1e1f0e 100644 --- a/.github/workflows/sentry_release.yml +++ b/.github/workflows/sentry_release.yml @@ -3,14 +3,14 @@ name: Create Sentry release on: push: branches: - - master + - main jobs: create_sentry_release: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@master + uses: actions/checkout@main - name: Create a Sentry.io release uses: tclindner/sentry-releases-action@v1.2.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be591d17e..addab32ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to one of Our Projects -Our projects are open-source and are automatically deployed whenever commits are pushed to the `master` branch on each repository, so we've created a set of guidelines in order to keep everything clean and in working order. +Our projects are open-source and are automatically deployed whenever commits are pushed to the `main` branch on each repository, so we've created a set of guidelines in order to keep everything clean and in working order. Note that contributions may be rejected on the basis of a contributor failing to follow these guidelines. @@ -8,7 +8,7 @@ Note that contributions may be rejected on the basis of a contributor failing to 1. **No force-pushes** or modifying the Git history in any way. 2. If you have direct access to the repository, **create a branch for your changes** and create a pull request for that branch. If not, create a branch on a fork of the repository and create a pull request from there. - * It's common practice for a repository to reject direct pushes to `master`, so make branching a habit! + * It's common practice for a repository to reject direct pushes to `main`, so make branching a habit! * If PRing from your own fork, **ensure that "Allow edits from maintainers" is checked**. This gives permission for maintainers to commit changes directly to your fork, speeding up the review process. 3. **Adhere to the prevailing code style**, which we enforce using [`flake8`](http://flake8.pycqa.org/en/latest/index.html) and [`pre-commit`](https://pre-commit.com/). * Run `flake8` and `pre-commit` against your code [**before** you push it](https://soundcloud.com/lemonsaurusrex/lint-before-you-push). Your commit will be rejected by the build server if it fails to lint. @@ -18,7 +18,7 @@ Note that contributions may be rejected on the basis of a contributor failing to * Avoid making minor commits for fixing typos or linting errors. Since you've already set up a `pre-commit` hook to run the linting pipeline before a commit, you shouldn't be committing linting issues anyway. * A more in-depth guide to writing great commit messages can be found in Chris Beam's [*How to Write a Git Commit Message*](https://chris.beams.io/posts/git-commit/) 5. **Avoid frequent pushes to the main repository**. This goes for PRs opened against your fork as well. Our test build pipelines are triggered every time a push to the repository (or PR) is made. Try to batch your commits until you've finished working for that session, or you've reached a point where collaborators need your commits to continue their own work. This also provides you the opportunity to amend commits for minor changes rather than having to commit them on their own because you've already pushed. - * This includes merging master into your branch. Try to leave merging from master for after your PR passes review; a maintainer will bring your PR up to date before merging. Exceptions to this include: resolving merge conflicts, needing something that was pushed to master for your branch, or something was pushed to master that could potentionally affect the functionality of what you're writing. + * This includes merging main into your branch. Try to leave merging from main for after your PR passes review; a maintainer will bring your PR up to date before merging. Exceptions to this include: resolving merge conflicts, needing something that was pushed to main for your branch, or something was pushed to main that could potentionally affect the functionality of what you're writing. 6. **Don't fight the framework**. Every framework has its flaws, but the frameworks we've picked out have been carefully chosen for their particular merits. If you can avoid it, please resist reimplementing swathes of framework logic - the work has already been done for you! 7. If someone is working on an issue or pull request, **do not open your own pull request for the same task**. Instead, collaborate with the author(s) of the existing pull request. Duplicate PRs opened without communicating with the other author(s) and/or PyDis staff will be closed. Communication is key, and there's no point in two separate implementations of the same thing. * One option is to fork the other contributor's repository and submit your changes to their branch with your own pull request. We suggest following these guidelines when interacting with their repository as well. diff --git a/README.md b/README.md index ac45e6340..9df905dc8 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ and other tools to help keep the server running like a well-oiled machine. Read the [Contributing Guide](https://pythondiscord.com/pages/contributing/bot/) on our website if you're interested in helping out. -[1]: https://github.com/python-discord/bot/workflows/Lint%20&%20Test/badge.svg?branch=master -[2]: https://github.com/python-discord/bot/actions?query=workflow%3A%22Lint+%26+Test%22+branch%3Amaster -[3]: https://github.com/python-discord/bot/workflows/Build/badge.svg?branch=master -[4]: https://github.com/python-discord/bot/actions?query=workflow%3ABuild+branch%3Amaster -[5]: https://github.com/python-discord/bot/workflows/Deploy/badge.svg?branch=master -[6]: https://github.com/python-discord/bot/actions?query=workflow%3ADeploy+branch%3Amaster -[7]: https://raw.githubusercontent.com/python-discord/branding/master/logos/badge/badge_github.svg +[1]: https://github.com/python-discord/bot/workflows/Lint%20&%20Test/badge.svg?branch=main +[2]: https://github.com/python-discord/bot/actions?query=workflow%3A%22Lint+%26+Test%22+branch%3Amain +[3]: https://github.com/python-discord/bot/workflows/Build/badge.svg?branch=main +[4]: https://github.com/python-discord/bot/actions?query=workflow%3ABuild+branch%3Amain +[5]: https://github.com/python-discord/bot/workflows/Deploy/badge.svg?branch=main +[6]: https://github.com/python-discord/bot/actions?query=workflow%3ADeploy+branch%3Amain +[7]: https://raw.githubusercontent.com/python-discord/branding/main/logos/badge/badge_github.svg [8]: https://discord.gg/python diff --git a/bot/exts/backend/branding/_constants.py b/bot/exts/backend/branding/_constants.py index dbc7615f2..ca8e8c5f5 100644 --- a/bot/exts/backend/branding/_constants.py +++ b/bot/exts/backend/branding/_constants.py @@ -42,7 +42,7 @@ SERVER_ICONS = "server_icons" BRANDING_URL = "https://api.github.com/repos/python-discord/branding/contents" -PARAMS = {"ref": "master"} # Target branch +PARAMS = {"ref": "main"} # Target branch HEADERS = {"Accept": "application/vnd.github.v3+json"} # Ensure we use API v3 # A GitHub token is not necessary for the cog to operate, diff --git a/bot/exts/backend/logging.py b/bot/exts/backend/logging.py index 94fa2b139..823f14ea4 100644 --- a/bot/exts/backend/logging.py +++ b/bot/exts/backend/logging.py @@ -29,7 +29,7 @@ class Logging(Cog): url="https://github.com/python-discord/bot", icon_url=( "https://raw.githubusercontent.com/" - "python-discord/branding/master/logos/logo_circle/logo_circle_large.png" + "python-discord/branding/main/logos/logo_circle/logo_circle_large.png" ) ) diff --git a/bot/exts/info/source.py b/bot/exts/info/source.py index 7b41352d4..49e74f204 100644 --- a/bot/exts/info/source.py +++ b/bot/exts/info/source.py @@ -97,7 +97,7 @@ class BotSource(commands.Cog): else: file_location = Path(filename).relative_to(Path.cwd()).as_posix() - url = f"{URLs.github_bot_repo}/blob/master/{file_location}{lines_extension}" + url = f"{URLs.github_bot_repo}/blob/main/{file_location}{lines_extension}" return url, file_location, first_line_no or None diff --git a/config-default.yml b/config-default.yml index 3dbc7bd6b..49d7f84ac 100644 --- a/config-default.yml +++ b/config-default.yml @@ -89,8 +89,8 @@ style: filtering: "https://cdn.discordapp.com/emojis/472472638594482195.png" - green_checkmark: "https://raw.githubusercontent.com/python-discord/branding/master/icons/checkmark/green-checkmark-dist.png" - green_questionmark: "https://raw.githubusercontent.com/python-discord/branding/master/icons/checkmark/green-question-mark-dist.png" + green_checkmark: "https://raw.githubusercontent.com/python-discord/branding/main/icons/checkmark/green-checkmark-dist.png" + green_questionmark: "https://raw.githubusercontent.com/python-discord/branding/main/icons/checkmark/green-question-mark-dist.png" guild_update: "https://cdn.discordapp.com/emojis/469954765141442561.png" hash_blurple: "https://cdn.discordapp.com/emojis/469950142942806017.png" @@ -360,8 +360,8 @@ urls: discord_api: &DISCORD_API "https://discordapp.com/api/v7/" discord_invite_api: !JOIN [*DISCORD_API, "invites"] - # Misc URLs - bot_avatar: "https://raw.githubusercontent.com/discord-python/branding/master/logos/logo_circle/logo_circle.png" + # Misc URLsw + bot_avatar: "https://raw.githubusercontent.com/python-discord/branding/main/logos/logo_circle/logo_circle.png" github_bot_repo: "https://github.com/python-discord/bot" -- cgit v1.2.3 From c0d8b0e781fdd7638d0a0f31d7a3317cdc797e5a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 13 Mar 2021 14:11:59 -0800 Subject: Use .gitattributes to normalise line endings on check-in Remove the mixed line endings pre-commit hook because it is obsolete. Relying on git to handle line endings means contributors have more flexibility with which line endings they want to use on check-out. The settings in .gitattributes only impose which line endings will be used upon check-in (LF), which should not impact local development; git will still respect the core.eol and core.autocrlf settings. --- .gitattributes | 1 + .pre-commit-config.yaml | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1597592ca..52500a282 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,8 +7,6 @@ repos: - id: check-yaml args: [--unsafe] # Required due to custom constructors (e.g. !ENV) - id: end-of-file-fixer - - id: mixed-line-ending - args: [--fix=lf] - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - repo: https://github.com/pre-commit/pygrep-hooks -- cgit v1.2.3 From 290a082207faa94dea0f468ef0cab793e1e2cae9 Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Sun, 14 Mar 2021 21:13:36 +0000 Subject: feat: add new discord.py tags --- bot/resources/tags/customhelp.md | 3 +++ bot/resources/tags/intents.md | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 bot/resources/tags/customhelp.md create mode 100644 bot/resources/tags/intents.md diff --git a/bot/resources/tags/customhelp.md b/bot/resources/tags/customhelp.md new file mode 100644 index 000000000..b787fe673 --- /dev/null +++ b/bot/resources/tags/customhelp.md @@ -0,0 +1,3 @@ +**Custom help commands in discord.py** + +To learn more about how to create custom help commands in discord.py by subclassing the help command, please see [this tutorial](https://gist.github.com/InterStella0/b78488fb28cadf279dfd3164b9f0cf96#embed-minimalhelpcommand) by Stella#2000 \ No newline at end of file diff --git a/bot/resources/tags/intents.md b/bot/resources/tags/intents.md new file mode 100644 index 000000000..642e65764 --- /dev/null +++ b/bot/resources/tags/intents.md @@ -0,0 +1,19 @@ +**Using intents in discord.py** + +Intents are a feature of Discord that tells the gateway exactly which events to send your bot. By default discord.py has all intents enabled, except for the `Members` and `Presences` intents, which are needed for events such as `on_member` and to get members' statuses. + +To enable one of these intents you need to first to to the [Discord developer portal](https://discord.com/developers/applications), then to the bot page of your bot's application. Scroll down to the `Privileged Gateway Intents` section, and enable the intents that you need. + +Next, in your bot you need to set the intents you want to connect with in the bot's constructor using the `intents` keyword argument, like this: + +```py +from discord import Intents +from discord.ext import commands + +intents = Intents.default() +intents.members = True + +bot = commands.Bot(command_prefix="!", intents=intents) +``` + +For more info about using intents, see the [discord.py docs on intents.](https://discordpy.readthedocs.io/en/latest/intents.html) \ No newline at end of file -- cgit v1.2.3 From 39b4da6a242a96ac298119d60f89bf2af69a952f Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Sun, 14 Mar 2021 21:16:42 +0000 Subject: fix: add newline file endings --- bot/resources/tags/customhelp.md | 2 +- bot/resources/tags/intents.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/customhelp.md b/bot/resources/tags/customhelp.md index b787fe673..6f0b17642 100644 --- a/bot/resources/tags/customhelp.md +++ b/bot/resources/tags/customhelp.md @@ -1,3 +1,3 @@ **Custom help commands in discord.py** -To learn more about how to create custom help commands in discord.py by subclassing the help command, please see [this tutorial](https://gist.github.com/InterStella0/b78488fb28cadf279dfd3164b9f0cf96#embed-minimalhelpcommand) by Stella#2000 \ No newline at end of file +To learn more about how to create custom help commands in discord.py by subclassing the help command, please see [this tutorial](https://gist.github.com/InterStella0/b78488fb28cadf279dfd3164b9f0cf96#embed-minimalhelpcommand) by Stella#2000 diff --git a/bot/resources/tags/intents.md b/bot/resources/tags/intents.md index 642e65764..9171b2314 100644 --- a/bot/resources/tags/intents.md +++ b/bot/resources/tags/intents.md @@ -16,4 +16,4 @@ intents.members = True bot = commands.Bot(command_prefix="!", intents=intents) ``` -For more info about using intents, see the [discord.py docs on intents.](https://discordpy.readthedocs.io/en/latest/intents.html) \ No newline at end of file +For more info about using intents, see the [discord.py docs on intents.](https://discordpy.readthedocs.io/en/latest/intents.html) -- cgit v1.2.3 From b8a74372c6f37c2eda28272195a96668d324844d Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Sun, 14 Mar 2021 21:44:13 +0000 Subject: fix: minor spelling correction --- bot/resources/tags/intents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/intents.md b/bot/resources/tags/intents.md index 9171b2314..0e94520a8 100644 --- a/bot/resources/tags/intents.md +++ b/bot/resources/tags/intents.md @@ -2,7 +2,7 @@ Intents are a feature of Discord that tells the gateway exactly which events to send your bot. By default discord.py has all intents enabled, except for the `Members` and `Presences` intents, which are needed for events such as `on_member` and to get members' statuses. -To enable one of these intents you need to first to to the [Discord developer portal](https://discord.com/developers/applications), then to the bot page of your bot's application. Scroll down to the `Privileged Gateway Intents` section, and enable the intents that you need. +To enable one of these intents you need to first go to the [Discord developer portal](https://discord.com/developers/applications), then to the bot page of your bot's application. Scroll down to the `Privileged Gateway Intents` section, and enable the intents that you need. Next, in your bot you need to set the intents you want to connect with in the bot's constructor using the `intents` keyword argument, like this: -- cgit v1.2.3 From 4fc4d1c0d0303ec7c207165bd812aeb1387e58ac Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Sun, 14 Mar 2021 21:51:38 +0000 Subject: fix: more minor spelling/grammar corrections --- bot/resources/tags/intents.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/intents.md b/bot/resources/tags/intents.md index 0e94520a8..6a282bc17 100644 --- a/bot/resources/tags/intents.md +++ b/bot/resources/tags/intents.md @@ -1,8 +1,8 @@ **Using intents in discord.py** -Intents are a feature of Discord that tells the gateway exactly which events to send your bot. By default discord.py has all intents enabled, except for the `Members` and `Presences` intents, which are needed for events such as `on_member` and to get members' statuses. +Intents are a feature of Discord that tells the gateway exactly which events to send your bot. By default, discord.py has all intents enabled, except for the `Members` and `Presences` intents, which are needed for events such as `on_member` and to get members' statuses. -To enable one of these intents you need to first go to the [Discord developer portal](https://discord.com/developers/applications), then to the bot page of your bot's application. Scroll down to the `Privileged Gateway Intents` section, and enable the intents that you need. +To enable one of these intents, you need to first go to the [Discord developer portal](https://discord.com/developers/applications), then to the bot page of your bot's application. Scroll down to the `Privileged Gateway Intents` section, then enable the intents that you need. Next, in your bot you need to set the intents you want to connect with in the bot's constructor using the `intents` keyword argument, like this: -- cgit v1.2.3 From e7302f0e50dfe158d3f4771d3e6d2181f5ac0351 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 15 Mar 2021 15:07:02 -0700 Subject: Code block: remove null bytes before parsing AST `ast.parse` raises a ValueError complaining that source code strings cannot contain null bytes. It seems like they may accidentally get pasted into Discord by users sometimes. --- bot/exts/info/codeblock/_parsing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/info/codeblock/_parsing.py b/bot/exts/info/codeblock/_parsing.py index e35fbca22..73fd11b94 100644 --- a/bot/exts/info/codeblock/_parsing.py +++ b/bot/exts/info/codeblock/_parsing.py @@ -103,6 +103,9 @@ def _is_python_code(content: str) -> bool: """Return True if `content` is valid Python consisting of more than just expressions.""" log.trace("Checking if content is Python code.") try: + # Remove null bytes because they cause ast.parse to raise a ValueError. + content = content.replace("\x00", "") + # Attempt to parse the message into an AST node. # Invalid Python code will raise a SyntaxError. tree = ast.parse(content) -- cgit v1.2.3 From 69ddce47076ef611cd250f6291d3dd0530b05790 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 15 Mar 2021 15:27:45 -0700 Subject: Defcon: fix naming conflict between threshold cmd and attribute --- bot/exts/moderation/defcon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index bd16289b9..bab95405c 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -157,9 +157,9 @@ class Defcon(Cog): await ctx.send(embed=embed) - @defcon_group.command(aliases=('t', 'd')) + @defcon_group.command(name="threshold", aliases=('t', 'd')) @has_any_role(*MODERATION_ROLES) - async def threshold( + async def threshold_command( self, ctx: Context, threshold: Union[DurationDelta, int], expiry: Optional[Expiry] = None ) -> None: """ -- cgit v1.2.3 From 089e4aaa6ac067b40d70b8cbbb95f9d26845d71f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 15 Mar 2021 15:32:31 -0700 Subject: Info: account for defcon threshold being None Fixes BOT-XK --- bot/exts/info/information.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 92ddf0fbd..c54ca96bf 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -64,7 +64,8 @@ class Information(Cog): defcon_info = "" if cog := self.bot.get_cog("Defcon"): - defcon_info = f"Defcon threshold: {humanize_delta(cog.threshold)}\n" + threshold = humanize_delta(cog.threshold) if cog.threshold else "-" + defcon_info = f"Defcon threshold: {threshold}\n" verification = f"Verification level: {ctx.guild.verification_level.name}\n" -- cgit v1.2.3 From 7aa572752ff24541b203f85fca1b74a66d226782 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 16:50:34 +0200 Subject: Apply requested style and grammar changes --- bot/exts/recruitment/talentpool/_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 682a32918..b4e425187 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -271,7 +271,7 @@ class Reviewer: On success, returns the user ID. """ - log.trace(f"Updating nomination #{nomination_id} as review") + log.trace(f"Updating nomination #{nomination_id} as reviewed") try: nomination = await self.bot.api_client.get(f"{self._pool.api_endpoint}/{nomination_id}") except ResponseCodeError as e: @@ -299,7 +299,7 @@ class Reviewer: def cancel(self, user_id: int) -> None: """ - Cancels the review of the nominee with ID user_id. + Cancels the review of the nominee with ID `user_id`. It's important to note that this applies only until reschedule_reviews is called again. To permanently cancel someone's review, either remove them from the pool, or use mark_reviewed. -- cgit v1.2.3 From 09d7f0775109224faa3a437bc65546d24ae3576f Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 17:04:13 +0200 Subject: Add additional logging to _review.py --- bot/exts/recruitment/talentpool/_review.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index b4e425187..920728544 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -127,11 +127,14 @@ class Reviewer: user_activity = await self.bot.api_client.get(f"bot/users/{member.id}/metricity_review_data") except ResponseCodeError as e: if e.status == 404: + log.trace(f"The user {member.id} seems to have no activity logged in Metricity.") messages = "no" channels = "" else: + log.trace(f"An unexpected error occured while fetching information of user {member.id}.") raise else: + log.trace(f"Activity found for {member.id}, formatting review.") messages = user_activity["total_messages"] # Making this part flexible to the amount of expected and returned channels. first_channel = user_activity["top_channel_activity"][0] @@ -164,6 +167,7 @@ class Reviewer: params={'user__id': str(member.id), 'ordering': '-inserted_at'} ) + log.trace(f"{len(infraction_list)} infractions found for {member.id}, formatting review.") if not infraction_list: return "They have no infractions." @@ -224,6 +228,7 @@ class Reviewer: } ) + log.trace(f"{len(history)} previous nominations found for {member.id}, formatting review.") if not history: return @@ -257,6 +262,7 @@ class Reviewer: Returns the resulting message objects. """ messages = textwrap.wrap(text, width=MAX_MESSAGE_SIZE, replace_whitespace=False) + log.trace(f"The provided string will be sent to the channel {channel.id} as {len(messages)} messages.") results = [] for message in messages: @@ -304,6 +310,7 @@ class Reviewer: It's important to note that this applies only until reschedule_reviews is called again. To permanently cancel someone's review, either remove them from the pool, or use mark_reviewed. """ + log.trace(f"Canceling the review of user {user_id}.") self._review_scheduler.cancel(user_id) def cancel_all(self) -> None: @@ -313,4 +320,5 @@ class Reviewer: It's important to note that this applies only until reschedule_reviews is called again. To permanently cancel someone's review, either remove them from the pool, or use mark_reviewed. """ + log.trace("Canceling all reviews.") self._review_scheduler.cancel_all() -- cgit v1.2.3 From 69c49a8ca9aaf552719e1045c7a4c99f73185d62 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 17:55:44 +0200 Subject: Use ctx.send instead of ctx.channel.send Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> --- bot/exts/recruitment/talentpool/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 7b21dcd53..f3e3539b6 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -306,7 +306,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): """Mark a nomination as reviewed and cancel the review task.""" if not await self.reviewer.mark_reviewed(ctx, nomination_id): return - await ctx.channel.send(f"✅ The nomination with ID `{nomination_id}` was marked as reviewed.") + await ctx.send(f"✅ The nomination with ID `{nomination_id}` was marked as reviewed.") @nomination_group.command(aliases=('review',)) @has_any_role(*MODERATION_ROLES) -- cgit v1.2.3 From 96003d7e388587a77d6f6424a1aa1c93d059be99 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 17:57:29 +0200 Subject: Properly await coroutine in post_review --- bot/exts/recruitment/talentpool/_review.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 920728544..5fb1a505f 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -76,7 +76,9 @@ class Reviewer: channel = guild.get_channel(Channels.mod_announcements) member = guild.get_member(user_id) if not member: - channel.send(f"I tried to review the user with ID `{user_id}`, but they don't appear to be on the server 😔") + await channel.send( + f"I tried to review the user with ID `{user_id}`, but they don't appear to be on the server 😔" + ) return if update_database: -- cgit v1.2.3 From 2d9c47180157e7b6667340abc241e0d65cdb9cc5 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 18:20:56 +0200 Subject: Replace mentions for ID's in watchlist lists Uncached mentions render as 'invalid' users on mobile, and with the list now showing the user's name we can now just show the ID without many problems. --- bot/exts/moderation/watchchannels/_watchchannel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index b121243ce..9f26c34f2 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -353,7 +353,7 @@ class WatchChannel(metaclass=CogABCMeta): list_data["info"] = {} for user_id, user_data in watched_iter: member = ctx.guild.get_member(user_id) - line = f"• <@{user_id}>" + line = f"• `{user_id}`" if member: line += f" ({member.name}#{member.discriminator})" inserted_at = user_data['inserted_at'] -- cgit v1.2.3 From 1127da5c9a50bd01155b993eb0bac3e540410df9 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 18:53:42 +0200 Subject: Default message in review when no nomination reason given --- bot/exts/recruitment/talentpool/_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 5fb1a505f..db710c278 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -87,7 +87,7 @@ class Reviewer: opening = f"<@&{Roles.moderators}> <@&{Roles.admins}>\n{member.mention} ({member}) for Helper!" current_nominations = "\n\n".join( - f"**<@{entry['actor']}>:** {entry['reason']}" for entry in nomination['entries'] + f"**<@{entry['actor']}>:** {entry['reason'] or '*no reason given*'}" for entry in nomination['entries'] ) current_nominations = f"**Nominated by:**\n{current_nominations}" -- cgit v1.2.3 From 94fc1cc0d4c7a69433c74eb555621374ac71ee22 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 20:21:16 +0200 Subject: Mark as reviewed when nominee is off server This is necessary as otherwise the bot would try to review them every time it restarts --- bot/exts/recruitment/talentpool/_review.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index db710c278..49aee8970 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -75,15 +75,16 @@ class Reviewer: guild = self.bot.get_guild(Guild.id) channel = guild.get_channel(Channels.mod_announcements) member = guild.get_member(user_id) + + if update_database: + await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True}) + if not member: await channel.send( f"I tried to review the user with ID `{user_id}`, but they don't appear to be on the server 😔" ) return - if update_database: - await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True}) - opening = f"<@&{Roles.moderators}> <@&{Roles.admins}>\n{member.mention} ({member}) for Helper!" current_nominations = "\n\n".join( -- cgit v1.2.3 From 3bf532b4ba499fc276c94f1cd6d3d859afbb925e Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 20:50:57 +0200 Subject: Don't reschedule reviews that are long overdue If it's been over a day overdue for a review, don't reschedule it. This is done in order to not fire reviews for all nominations which are over 30 days old when the auto-reviewing feature is merged. --- bot/exts/recruitment/talentpool/_review.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 49aee8970..ba1564602 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -61,7 +61,9 @@ class Reviewer: inserted_at = isoparse(user_data['inserted_at']).replace(tzinfo=None) review_at = inserted_at + timedelta(days=MAX_DAYS_IN_POOL) - self._review_scheduler.schedule_at(review_at, user_id, self.post_review(user_id, update_database=True)) + # If it's over a day overdue, it's probably an old nomination and shouldn't be automatically reviewed. + if datetime.utcnow() - review_at < timedelta(days=1): + self._review_scheduler.schedule_at(review_at, user_id, self.post_review(user_id, update_database=True)) async def post_review(self, user_id: int, update_database: bool) -> None: """Format a generic review of a user and post it to the mod announcements channel.""" -- cgit v1.2.3 From a7c85564d90a3dc556a9582e925b33adc303de8f Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 22:35:00 +0200 Subject: Review commands now use the user ID instead of nomination ID The user ID is much more accessible, and is usually what is used to obtain the nomination ID. --- bot/exts/recruitment/talentpool/_cog.py | 12 +++++------ bot/exts/recruitment/talentpool/_review.py | 34 ++++++++++++------------------ 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index f3e3539b6..b809cea17 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -302,17 +302,17 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"): @nomination_group.command(aliases=('mr',)) @has_any_role(*MODERATION_ROLES) - async def mark_reviewed(self, ctx: Context, nomination_id: int) -> None: - """Mark a nomination as reviewed and cancel the review task.""" - if not await self.reviewer.mark_reviewed(ctx, nomination_id): + async def mark_reviewed(self, ctx: Context, user_id: int) -> None: + """Mark a user's nomination as reviewed and cancel the review task.""" + if not await self.reviewer.mark_reviewed(ctx, user_id): return - await ctx.send(f"✅ The nomination with ID `{nomination_id}` was marked as reviewed.") + await ctx.send(f"✅ The user with ID `{user_id}` was marked as reviewed.") @nomination_group.command(aliases=('review',)) @has_any_role(*MODERATION_ROLES) - async def post_review(self, ctx: Context, nomination_id: int) -> None: + async def post_review(self, ctx: Context, user_id: int) -> None: """Post the automatic review for the user ahead of time.""" - if not (user_id := await self.reviewer.mark_reviewed(ctx, nomination_id)): + if not await self.reviewer.mark_reviewed(ctx, user_id): return await self.reviewer.post_review(user_id, update_database=False) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index ba1564602..c2c1312d9 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -276,37 +276,29 @@ class Reviewer: return results - async def mark_reviewed(self, ctx: Context, nomination_id: int) -> Optional[int]: + async def mark_reviewed(self, ctx: Context, user_id: int) -> bool: """ Mark an active nomination as reviewed, updating the database and canceling the review task. - On success, returns the user ID. + Returns True if the user was successfully marked as reviewed, False otherwise. """ - log.trace(f"Updating nomination #{nomination_id} as reviewed") - try: - nomination = await self.bot.api_client.get(f"{self._pool.api_endpoint}/{nomination_id}") - except ResponseCodeError as e: - if e.response.status == 404: - log.trace(f"Nomination API 404: Can't find nomination with id {nomination_id}") - await ctx.send(f"❌ Can't find a nomination with id `{nomination_id}`") - return - else: - raise + log.trace(f"Updating user {user_id} as reviewed") + await self._pool.fetch_user_cache() + if user_id not in self._pool.watched_users: + log.trace(f"Can't find a nominated user with id {user_id}") + await ctx.send(f"❌ Can't find a currently nominated user with id `{user_id}`") + return False + nomination = self._pool.watched_users[user_id] if nomination["reviewed"]: await ctx.send("❌ This nomination was already reviewed, but here's a cookie 🍪") - return - elif not nomination["active"]: - await ctx.send("❌ This nomination is inactive") - return + return False await self.bot.api_client.patch(f"{self._pool.api_endpoint}/{nomination['id']}", json={"reviewed": True}) - if nomination["user"] in self._review_scheduler: - self._review_scheduler.cancel(nomination["user"]) - - await self._pool.fetch_user_cache() + if user_id in self._review_scheduler: + self._review_scheduler.cancel(user_id) - return nomination["user"] + return True def cancel(self, user_id: int) -> None: """ -- cgit v1.2.3 From e69e918a4309c04c3786da9c0d81e81540ffe411 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 19 Mar 2021 23:35:10 +0200 Subject: Fix review formatting when there's only one infraction type --- bot/exts/recruitment/talentpool/_review.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index c2c1312d9..57e18af9a 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -180,11 +180,14 @@ class Reviewer: infr_stats = list(Counter(infr["type"] for infr in infraction_list).items()) # Format into a sentence. - infractions = ", ".join( - f"{count} {self._format_infr_name(infr_type, count)}" - for infr_type, count in infr_stats[:-1] - ) - if len(infr_stats) > 1: + if len(infr_stats) == 1: + infr_type, count = infr_stats[0] + infractions = f"{count} {self._format_infr_name(infr_type, count)}" + else: # We already made sure they have infractions. + infractions = ", ".join( + f"{count} {self._format_infr_name(infr_type, count)}" + for infr_type, count in infr_stats[:-1] + ) last_infr, last_count = infr_stats[-1] infractions += f", and {last_count} {self._format_infr_name(last_infr, last_count)}" -- cgit v1.2.3 From 350f02fab382810824b464889a8e9d29fb8407ce Mon Sep 17 00:00:00 2001 From: wookie184 Date: Sat, 20 Mar 2021 15:40:42 +0000 Subject: Added nomination voting channel to config Also changed talentpool review cog to post there instead of mod-announcements --- bot/constants.py | 1 + bot/exts/recruitment/talentpool/_review.py | 4 ++-- config-default.yml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 394d59a73..467a4a2c4 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -438,6 +438,7 @@ class Channels(metaclass=YAMLGetter): mods: int mod_alerts: int mod_spam: int + nomination_voting: int organisation: int admin_announcements: int diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 57e18af9a..fb3461238 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -66,7 +66,7 @@ class Reviewer: self._review_scheduler.schedule_at(review_at, user_id, self.post_review(user_id, update_database=True)) async def post_review(self, user_id: int, update_database: bool) -> None: - """Format a generic review of a user and post it to the mod announcements channel.""" + """Format a generic review of a user and post it to the nomination voting channel.""" log.trace(f"Posting the review of {user_id}") nomination = self._pool.watched_users[user_id] @@ -75,7 +75,7 @@ class Reviewer: return guild = self.bot.get_guild(Guild.id) - channel = guild.get_channel(Channels.mod_announcements) + channel = guild.get_channel(Channels.nomination_voting) member = guild.get_member(user_id) if update_database: diff --git a/config-default.yml b/config-default.yml index 49d7f84ac..502f0f861 100644 --- a/config-default.yml +++ b/config-default.yml @@ -199,6 +199,7 @@ guild: mod_meta: &MOD_META 775412552795947058 mod_spam: &MOD_SPAM 620607373828030464 mod_tools: &MOD_TOOLS 775413915391098921 + nomination_voting: 822853512709931008 organisation: &ORGANISATION 551789653284356126 staff_lounge: &STAFF_LOUNGE 464905259261755392 -- cgit v1.2.3 From 8747a66b3133c5b942a3f10b7ede313e93120038 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Sat, 20 Mar 2021 18:21:21 +0000 Subject: Added myself to CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7217cb443..634bb4bca 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,6 +12,7 @@ bot/exts/info/information.py @mbaruh bot/exts/filters/** @mbaruh bot/exts/fun/** @ks129 bot/exts/utils/** @ks129 +bot/exts/recruitment/** @wookie184 # Rules bot/rules/** @mbaruh -- cgit v1.2.3 From bd64acac079c564d3fca64519463518f7056dfe2 Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Fri, 26 Mar 2021 17:49:15 +0000 Subject: fix: remove . from the hyperlink Co-authored-by: Joe Banks <20439493+jb3@users.noreply.github.com> --- bot/resources/tags/intents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/intents.md b/bot/resources/tags/intents.md index 6a282bc17..e08fd1c33 100644 --- a/bot/resources/tags/intents.md +++ b/bot/resources/tags/intents.md @@ -16,4 +16,4 @@ intents.members = True bot = commands.Bot(command_prefix="!", intents=intents) ``` -For more info about using intents, see the [discord.py docs on intents.](https://discordpy.readthedocs.io/en/latest/intents.html) +For more info about using intents, see the [discord.py docs on intents](https://discordpy.readthedocs.io/en/latest/intents.html). -- cgit v1.2.3 From 2cf2402ea51e3a61d319706a95bc4ab633d6b8fc Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Fri, 26 Mar 2021 17:52:07 +0000 Subject: feat: add link to discord dev portal intents section --- bot/resources/tags/intents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/intents.md b/bot/resources/tags/intents.md index e08fd1c33..464caf0ba 100644 --- a/bot/resources/tags/intents.md +++ b/bot/resources/tags/intents.md @@ -16,4 +16,4 @@ intents.members = True bot = commands.Bot(command_prefix="!", intents=intents) ``` -For more info about using intents, see the [discord.py docs on intents](https://discordpy.readthedocs.io/en/latest/intents.html). +For more info about using intents, see the [discord.py docs on intents](https://discordpy.readthedocs.io/en/latest/intents.html), and for general information about them, see the [Discord developer documentation on intents](https://discord.com/developers/docs/topics/gateway#gateway-intents). -- cgit v1.2.3