From b9d483c15464f4b11575090b27306f2accc47acf Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 20 Jun 2020 08:47:24 +0300 Subject: Watchchannel: Moved message consuming task cancelling exception Moved exception logging when cog is being unloaded and messages is still not consumed from `cog_unload` to `consume_messages` itself in try-except block to avoid case when requesting result too early (before cancel finished). --- bot/cogs/watchchannels/watchchannel.py | 53 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py index 436778c46..d78d45f26 100644 --- a/bot/cogs/watchchannels/watchchannel.py +++ b/bot/cogs/watchchannels/watchchannel.py @@ -169,32 +169,38 @@ class WatchChannel(metaclass=CogABCMeta): async def consume_messages(self, delay_consumption: bool = True) -> None: """Consumes the message queues to log watched users' messages.""" - if delay_consumption: - self.log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") - await asyncio.sleep(BigBrotherConfig.log_delay) + try: + if delay_consumption: + self.log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") + await asyncio.sleep(BigBrotherConfig.log_delay) - self.log.trace("Started consuming the message queue") + self.log.trace("Started consuming the message queue") - # If the previous consumption Task failed, first consume the existing comsumption_queue - if not self.consumption_queue: - self.consumption_queue = self.message_queue.copy() - self.message_queue.clear() + # If the previous consumption Task failed, first consume the existing comsumption_queue + if not self.consumption_queue: + self.consumption_queue = self.message_queue.copy() + self.message_queue.clear() - for user_channel_queues in self.consumption_queue.values(): - for channel_queue in user_channel_queues.values(): - while channel_queue: - msg = channel_queue.popleft() + for user_channel_queues in self.consumption_queue.values(): + for channel_queue in user_channel_queues.values(): + while channel_queue: + msg = channel_queue.popleft() - self.log.trace(f"Consuming message {msg.id} ({len(msg.attachments)} attachments)") - await self.relay_message(msg) + self.log.trace(f"Consuming message {msg.id} ({len(msg.attachments)} attachments)") + await self.relay_message(msg) - self.consumption_queue.clear() + self.consumption_queue.clear() - if self.message_queue: - self.log.trace("Channel queue not empty: Continuing consuming queues") - self._consume_task = self.bot.loop.create_task(self.consume_messages(delay_consumption=False)) - else: - self.log.trace("Done consuming messages.") + if self.message_queue: + self.log.trace("Channel queue not empty: Continuing consuming queues") + self._consume_task = self.bot.loop.create_task(self.consume_messages(delay_consumption=False)) + else: + self.log.trace("Done consuming messages.") + except asyncio.CancelledError as e: + self.log.exception( + "The consume task was canceled. Messages may be lost.", + exc_info=e + ) async def webhook_send( self, @@ -330,10 +336,3 @@ class WatchChannel(metaclass=CogABCMeta): self.log.trace("Unloading the cog") if self._consume_task and not self._consume_task.done(): self._consume_task.cancel() - try: - self._consume_task.result() - except asyncio.CancelledError as e: - self.log.exception( - "The consume task was canceled. Messages may be lost.", - exc_info=e - ) -- cgit v1.2.3 From ac302d3d2360c3b379632ce033884127321a76b5 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 20 Jun 2020 12:08:29 +0300 Subject: Infractions: Fix cases when user leave from guild before assigning roles When user left from guild before bot can add Muted role, then catch this error and log. --- bot/cogs/moderation/infractions.py | 11 +++++++---- bot/cogs/moderation/scheduler.py | 9 +++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/bot/cogs/moderation/infractions.py b/bot/cogs/moderation/infractions.py index 3b28526b2..c03c8d974 100644 --- a/bot/cogs/moderation/infractions.py +++ b/bot/cogs/moderation/infractions.py @@ -223,10 +223,13 @@ class Infractions(InfractionScheduler, commands.Cog): self.mod_log.ignore(Event.member_update, user.id) async def action() -> None: - await user.add_roles(self._muted_role, reason=reason) - - log.trace(f"Attempting to kick {user} from voice because they've been muted.") - await user.move_to(None, reason=reason) + try: + await user.add_roles(self._muted_role, reason=reason) + except discord.NotFound: + log.info(f"User {user} ({user.id}) left from guild. Can't give Muted role.") + else: + log.trace(f"Attempting to kick {user} from voice because they've been muted.") + await user.move_to(None, reason=reason) await self.apply_infraction(ctx, infraction, user, action()) diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py index d75a72ddb..28547545e 100644 --- a/bot/cogs/moderation/scheduler.py +++ b/bot/cogs/moderation/scheduler.py @@ -71,8 +71,13 @@ class InfractionScheduler(Scheduler): return # Allowing mod log since this is a passive action that should be logged. - await apply_coro - log.info(f"Re-applied {infraction['type']} to user {infraction['user']} upon rejoining.") + try: + await apply_coro + except discord.NotFound: + # When user joined and then right after this left again before action completed, this can't add roles + log.info(f"Can't reapply {infraction['type']} to user {infraction['user']} because user left again.") + else: + log.info(f"Re-applied {infraction['type']} to user {infraction['user']} upon rejoining.") async def apply_infraction( self, -- cgit v1.2.3 From 429cc865309242f0cf37147f9c3f05036972eb8c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 20 Jun 2020 21:33:36 +0300 Subject: Implement bot closing tasks waiting + breaking `close` to multiple parts Made to resolve problem with Reddit cog that revoking access token raise exception because session is closed. To solve this, I made `Bot.closing_tasks` that bot wait before closing. Moved all extensions and cogs removing to `remove_extension` what is called before closing everything else because need to call `cog_unload`. --- bot/bot.py | 30 ++++++++++++++++++++++++++++-- bot/cogs/reddit.py | 4 +++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 313652d11..c9eb24bb5 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -2,7 +2,7 @@ import asyncio import logging import socket import warnings -from typing import Optional +from typing import List, Optional import aiohttp import aioredis @@ -49,6 +49,9 @@ class Bot(commands.Bot): self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") + # All tasks that need to block closing until finished + self.closing_tasks: List[asyncio.Task] = [] + async def _create_redis_session(self) -> None: """ Create the Redis connection pool, and then open the redis event gate. @@ -89,9 +92,32 @@ class Bot(commands.Bot): self._recreate() super().clear() + def remove_extensions(self) -> None: + """Remove all extensions and Cog to close bot. Copy from discord.py's own `close` for right closing order.""" + for extension in tuple(self.extensions): + try: + self.unload_extension(extension) + except Exception: + pass + + for cog in tuple(self.cogs): + try: + self.remove_cog(cog) + except Exception: + pass + async def close(self) -> None: """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" - await super().close() + # Remove extensions and cogs before calling super().close() to allow task finish before HTTP session close + self.remove_extensions() + + # Wait until all tasks that have to be completed before bot is closing is done + for task in self.closing_tasks: + log.trace(f"Waiting for task {task.get_name()} before closing.") + await task + + # Now actually do full close of bot + await super(commands.Bot, self).close() await self.api_client.close() diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index 3b77538a0..5a63d71fc 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -44,7 +44,9 @@ class Reddit(Cog): """Stop the loop task and revoke the access token when the cog is unloaded.""" self.auto_poster_loop.cancel() if self.access_token and self.access_token.expires_at > datetime.utcnow(): - asyncio.create_task(self.revoke_access_token()) + task = asyncio.create_task(self.revoke_access_token()) + task.set_name("revoke_reddit_access_token") + self.bot.closing_tasks.append(task) async def init_reddit_ready(self) -> None: """Sets the reddit webhook when the cog is loaded.""" -- cgit v1.2.3 From 177e4d4f68f407ac2808b18badd32a29d26034ff Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 21 Jun 2020 08:22:56 +0300 Subject: Reddit: Remove unnecessary revoke task name changing --- bot/cogs/reddit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index 5a63d71fc..681d1997f 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -45,7 +45,6 @@ class Reddit(Cog): self.auto_poster_loop.cancel() if self.access_token and self.access_token.expires_at > datetime.utcnow(): task = asyncio.create_task(self.revoke_access_token()) - task.set_name("revoke_reddit_access_token") self.bot.closing_tasks.append(task) async def init_reddit_ready(self) -> None: -- cgit v1.2.3 From 1fd30faaeaa2dfc3e38426db9112628bfdba0f04 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 21 Jun 2020 09:29:22 +0300 Subject: Reddit: Don't define revoke task as variable but instantly append --- bot/cogs/reddit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/cogs/reddit.py b/bot/cogs/reddit.py index 681d1997f..850d3afb2 100644 --- a/bot/cogs/reddit.py +++ b/bot/cogs/reddit.py @@ -44,8 +44,7 @@ class Reddit(Cog): """Stop the loop task and revoke the access token when the cog is unloaded.""" self.auto_poster_loop.cancel() if self.access_token and self.access_token.expires_at > datetime.utcnow(): - task = asyncio.create_task(self.revoke_access_token()) - self.bot.closing_tasks.append(task) + self.bot.closing_tasks.append(asyncio.create_task(self.revoke_access_token())) async def init_reddit_ready(self) -> None: """Sets the reddit webhook when the cog is loaded.""" -- cgit v1.2.3 From f4004d814c1babfb5906afb8cd9944ceef90a2a3 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 21 Jun 2020 09:30:47 +0300 Subject: Silence: Add mod alert sending to `closing_tasks` to avoid error --- bot/cogs/moderation/silence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/moderation/silence.py b/bot/cogs/moderation/silence.py index c8ab6443b..34baa2bcb 100644 --- a/bot/cogs/moderation/silence.py +++ b/bot/cogs/moderation/silence.py @@ -176,7 +176,7 @@ class Silence(Scheduler, commands.Cog): if self.muted_channels: channels_string = ''.join(channel.mention for channel in self.muted_channels) message = f"<@&{Roles.moderators}> channels left silenced on cog unload: {channels_string}" - asyncio.create_task(self._mod_alerts_channel.send(message)) + self.bot.closing_tasks.append(asyncio.create_task(self._mod_alerts_channel.send(message))) # This cannot be static (must have a __func__ attribute). def cog_check(self, ctx: Context) -> bool: -- cgit v1.2.3 From b040a38ea1e3c7baddb54395a1f09d11fdd4e818 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 23 Jul 2020 07:51:56 +0300 Subject: Add copyright about `_remove_extension` + make function private --- bot/bot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index c9eb24bb5..f5f76b7f8 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -92,8 +92,8 @@ class Bot(commands.Bot): self._recreate() super().clear() - def remove_extensions(self) -> None: - """Remove all extensions and Cog to close bot. Copy from discord.py's own `close` for right closing order.""" + def _remove_extensions(self) -> None: + """Remove all extensions and Cog to close bot. Copyright (c) 2015-2020 Rapptz (discord.py, MIT License).""" for extension in tuple(self.extensions): try: self.unload_extension(extension) -- cgit v1.2.3 From 360ce808bdc12ab8dfc998927d6a07658aa2b633 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 23 Jul 2020 07:56:12 +0300 Subject: Improve extension + cogs removing comment on `close` Co-authored-by: Mark --- bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/bot.py b/bot/bot.py index f5f76b7f8..7a8f9932c 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -108,7 +108,7 @@ class Bot(commands.Bot): async def close(self) -> None: """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" - # Remove extensions and cogs before calling super().close() to allow task finish before HTTP session close + # Done before super().close() to allow tasks finish before the HTTP session closes. self.remove_extensions() # Wait until all tasks that have to be completed before bot is closing is done -- cgit v1.2.3 From 65c4312515de65a59b7553b0581c31d0d9fa098b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 31 Jul 2020 18:00:36 +0300 Subject: Simplify bot shutdown cogs removing Unloading extensions already remove all cogs that is inside it and this is enough good for this case, because bot still call dpy's internal function later to remove cogs not related with extensions (when exist). --- bot/bot.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 7a8f9932c..5e05d1596 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -93,16 +93,10 @@ class Bot(commands.Bot): super().clear() def _remove_extensions(self) -> None: - """Remove all extensions and Cog to close bot. Copyright (c) 2015-2020 Rapptz (discord.py, MIT License).""" - for extension in tuple(self.extensions): + """Remove all extensions to trigger cog unloads.""" + for ext in self.extensions.keys(): try: - self.unload_extension(extension) - except Exception: - pass - - for cog in tuple(self.cogs): - try: - self.remove_cog(cog) + self.unload_extension(ext) except Exception: pass -- cgit v1.2.3 From 2dc0ee180330bcf2687d62e174abeea79e963775 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 31 Jul 2020 18:04:38 +0300 Subject: Use asyncio.gather instead manual looping and awaiting --- bot/bot.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 5e05d1596..2f366a3ef 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -106,9 +106,8 @@ class Bot(commands.Bot): self.remove_extensions() # Wait until all tasks that have to be completed before bot is closing is done - for task in self.closing_tasks: - log.trace(f"Waiting for task {task.get_name()} before closing.") - await task + log.trace("Waiting for tasks before closing.") + await asyncio.gather(*self.closing_tasks) # Now actually do full close of bot await super(commands.Bot, self).close() -- cgit v1.2.3 From 040ac421a26a270e64d9ed745fe28ee886181fed Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 9 Oct 2020 18:59:58 +0300 Subject: Make bot shutdown remove all other non-extension cogs again --- bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/bot.py b/bot/bot.py index 2f366a3ef..10c4c901b 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -110,7 +110,7 @@ class Bot(commands.Bot): await asyncio.gather(*self.closing_tasks) # Now actually do full close of bot - await super(commands.Bot, self).close() + await super().close() await self.api_client.close() -- cgit v1.2.3 From ff5c90bf12f14abb4d0a5bc73af435e53ffc7e3e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Fri, 9 Oct 2020 19:35:33 +0300 Subject: Fix calling extensions removing function with wrong name --- bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/bot.py b/bot/bot.py index b51e41117..e6d77344e 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -145,7 +145,7 @@ class Bot(commands.Bot): async def close(self) -> None: """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" # Done before super().close() to allow tasks finish before the HTTP session closes. - self.remove_extensions() + self._remove_extensions() # Wait until all tasks that have to be completed before bot is closing is done log.trace("Waiting for tasks before closing.") -- cgit v1.2.3 From 5a5a948efd954c8e878db50b6a5ec480fd97b3ec Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 08:13:19 +0300 Subject: Fix name of extensions removing function --- bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/bot.py b/bot/bot.py index b51e41117..e6d77344e 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -145,7 +145,7 @@ class Bot(commands.Bot): async def close(self) -> None: """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" # Done before super().close() to allow tasks finish before the HTTP session closes. - self.remove_extensions() + self._remove_extensions() # Wait until all tasks that have to be completed before bot is closing is done log.trace("Waiting for tasks before closing.") -- cgit v1.2.3 From b702618d8a9189e19c3107c79e23105e288798b0 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 08:38:49 +0300 Subject: Get all extensions first for unloading to avoid iteration error --- bot/bot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/bot.py b/bot/bot.py index e6d77344e..9a60474b3 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -136,7 +136,9 @@ class Bot(commands.Bot): def _remove_extensions(self) -> None: """Remove all extensions to trigger cog unloads.""" - for ext in self.extensions.keys(): + extensions = list(self.extensions.keys()) + + for ext in extensions: try: self.unload_extension(ext) except Exception: -- cgit v1.2.3 From d0af250507371739c652abfcc47efa4a86ce1166 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 08:41:32 +0300 Subject: Use done callback instead of plain try-except inside function --- bot/exts/moderation/watchchannels/_watchchannel.py | 56 ++++++++++++---------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index 4715dce14..b576f2888 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -171,38 +171,32 @@ class WatchChannel(metaclass=CogABCMeta): async def consume_messages(self, delay_consumption: bool = True) -> None: """Consumes the message queues to log watched users' messages.""" - try: - if delay_consumption: - self.log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") - await asyncio.sleep(BigBrotherConfig.log_delay) + if delay_consumption: + self.log.trace(f"Sleeping {BigBrotherConfig.log_delay} seconds before consuming message queue") + await asyncio.sleep(BigBrotherConfig.log_delay) - self.log.trace("Started consuming the message queue") + self.log.trace("Started consuming the message queue") - # If the previous consumption Task failed, first consume the existing comsumption_queue - if not self.consumption_queue: - self.consumption_queue = self.message_queue.copy() - self.message_queue.clear() + # If the previous consumption Task failed, first consume the existing comsumption_queue + if not self.consumption_queue: + self.consumption_queue = self.message_queue.copy() + self.message_queue.clear() - for user_channel_queues in self.consumption_queue.values(): - for channel_queue in user_channel_queues.values(): - while channel_queue: - msg = channel_queue.popleft() + for user_channel_queues in self.consumption_queue.values(): + for channel_queue in user_channel_queues.values(): + while channel_queue: + msg = channel_queue.popleft() - self.log.trace(f"Consuming message {msg.id} ({len(msg.attachments)} attachments)") - await self.relay_message(msg) + self.log.trace(f"Consuming message {msg.id} ({len(msg.attachments)} attachments)") + await self.relay_message(msg) - self.consumption_queue.clear() + self.consumption_queue.clear() - if self.message_queue: - self.log.trace("Channel queue not empty: Continuing consuming queues") - self._consume_task = self.bot.loop.create_task(self.consume_messages(delay_consumption=False)) - else: - self.log.trace("Done consuming messages.") - except asyncio.CancelledError as e: - self.log.exception( - "The consume task was canceled. Messages may be lost.", - exc_info=e - ) + if self.message_queue: + self.log.trace("Channel queue not empty: Continuing consuming queues") + self._consume_task = self.bot.loop.create_task(self.consume_messages(delay_consumption=False)) + else: + self.log.trace("Done consuming messages.") async def webhook_send( self, @@ -348,4 +342,14 @@ class WatchChannel(metaclass=CogABCMeta): """Takes care of unloading the cog and canceling the consumption task.""" self.log.trace("Unloading the cog") if self._consume_task and not self._consume_task.done(): + def done_callback(task: asyncio.Task) -> None: + """Send exception when consuming task have been cancelled.""" + try: + task.exception() + except asyncio.CancelledError: + self.log.error( + f"The consume task of {type(self).__name__} was canceled. Messages may be lost." + ) + + self._consume_task.add_done_callback(done_callback) self._consume_task.cancel() -- cgit v1.2.3 From 8ed147c402a3a6b5e98b29c3ed385460f3216efd Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 10 Oct 2020 09:05:00 +0300 Subject: Catch HTTPException when muting user --- bot/exts/moderation/infraction/infractions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index ccddd4530..b638f4dc6 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -242,8 +242,13 @@ class Infractions(InfractionScheduler, commands.Cog): async def action() -> None: try: await user.add_roles(self._muted_role, reason=reason) - except discord.NotFound: - log.info(f"User {user} ({user.id}) left from guild. Can't give Muted role.") + except discord.HTTPException as e: + if e.code == 10007: + log.info(f"User {user} ({user.id}) left from guild. Can't give Muted role.") + else: + log.warning( + f"Got response {e.code} (HTTP {e.status}) while giving muted role to {user} ({user.id})." + ) else: log.trace(f"Attempting to kick {user} from voice because they've been muted.") await user.move_to(None, reason=reason) -- cgit v1.2.3 From 027666f95ccaf07dfc73d2bfb7487e5a61bcd2d2 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 7 Nov 2020 09:45:19 +0200 Subject: Remove both cogs and extensions on closing --- bot/bot.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 9a60474b3..fbd97dc18 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -3,6 +3,7 @@ import logging import socket import warnings from collections import defaultdict +from contextlib import suppress from typing import Dict, List, Optional import aiohttp @@ -134,20 +135,12 @@ class Bot(commands.Bot): self._recreate() super().clear() - def _remove_extensions(self) -> None: - """Remove all extensions to trigger cog unloads.""" - extensions = list(self.extensions.keys()) - - for ext in extensions: - try: - self.unload_extension(ext) - except Exception: - pass - async def close(self) -> None: """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" # Done before super().close() to allow tasks finish before the HTTP session closes. - self._remove_extensions() + with suppress(Exception): + [self.unload_extension(ext) for ext in tuple(self.extensions)] + [self.remove_cog(cog) for cog in tuple(self.cogs)] # Wait until all tasks that have to be completed before bot is closing is done log.trace("Waiting for tasks before closing.") -- cgit v1.2.3 From d15c4fc004e73669014baa25c675a7bf7b8064f9 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 7 Nov 2020 09:47:05 +0200 Subject: Use result instead exception for watchchannel closing task --- 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 b576f2888..8894762f3 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -345,7 +345,7 @@ class WatchChannel(metaclass=CogABCMeta): def done_callback(task: asyncio.Task) -> None: """Send exception when consuming task have been cancelled.""" try: - task.exception() + task.result() except asyncio.CancelledError: self.log.error( f"The consume task of {type(self).__name__} was canceled. Messages may be lost." -- cgit v1.2.3 From 6a81f714c6648d7dd12982b38c7161cdee9e602e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 7 Nov 2020 09:57:23 +0200 Subject: Catch not found exception in scheduler --- bot/exts/moderation/infraction/_scheduler.py | 29 ++++++++++++++++++++++++--- bot/exts/moderation/infraction/infractions.py | 16 ++++----------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index ed67e3b26..6efa5b1e0 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -79,6 +79,16 @@ class InfractionScheduler: except discord.NotFound: # When user joined and then right after this left again before action completed, this can't add roles log.info(f"Can't reapply {infraction['type']} to user {infraction['user']} because user left again.") + except discord.HTTPException as e: + if e.code == 10007: + log.info(f"Can't reapply {infraction['type']} to user {infraction['user']} because user left again.") + else: + log.warning( + ( + f"Got unexpected HTTPException (HTTP {e.status}, Discord code {e.code})" + f"when awaiting {infraction['type']} coroutine for {infraction['user']}." + ) + ) else: log.info(f"Re-applied {infraction['type']} to user {infraction['user']} upon rejoining.") @@ -160,6 +170,8 @@ class InfractionScheduler: if expiry: # Schedule the expiration of the infraction. self.schedule_expiration(infraction) + except discord.NotFound: + log.info(f"Can't apply {infraction['type']} to user {infraction['user']} because user left from guild.") except discord.HTTPException as e: # Accordingly display that applying the infraction failed. # Don't use ctx.message.author; antispam only patches ctx.author. @@ -171,6 +183,10 @@ class InfractionScheduler: log_msg = f"Failed to apply {' '.join(infr_type.split('_'))} infraction #{id_} to {user}" if isinstance(e, discord.Forbidden): log.warning(f"{log_msg}: bot lacks permissions.") + elif e.code == 10007: + log.info( + f"Can't apply {infraction['type']} to user {infraction['user']} because user left from guild." + ) else: log.exception(log_msg) failed = True @@ -342,10 +358,17 @@ class InfractionScheduler: log.warning(f"Failed to deactivate infraction #{id_} ({type_}): bot lacks permissions.") log_text["Failure"] = "The bot lacks permissions to do this (role hierarchy?)" log_content = mod_role.mention + except discord.NotFound: + log.info(f"Can't pardon {infraction['type']} for user {infraction['user']} because user left from guild.") except discord.HTTPException as e: - log.exception(f"Failed to deactivate infraction #{id_} ({type_})") - log_text["Failure"] = f"HTTPException with status {e.status} and code {e.code}." - log_content = mod_role.mention + if e.code == 10007: + log.info( + f"Can't pardon {infraction['type']} for user {infraction['user']} because user left from guild." + ) + else: + log.exception(f"Failed to deactivate infraction #{id_} ({type_})") + log_text["Failure"] = f"HTTPException with status {e.status} and code {e.code}." + log_content = mod_role.mention # Check if the user is currently being watched by Big Brother. try: diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 8abb199db..746d4e154 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -277,18 +277,10 @@ class Infractions(InfractionScheduler, commands.Cog): self.mod_log.ignore(Event.member_update, user.id) async def action() -> None: - try: - await user.add_roles(self._muted_role, reason=reason) - except discord.HTTPException as e: - if e.code == 10007: - log.info(f"User {user} ({user.id}) left from guild. Can't give Muted role.") - else: - log.warning( - f"Got response {e.code} (HTTP {e.status}) while giving muted role to {user} ({user.id})." - ) - else: - log.trace(f"Attempting to kick {user} from voice because they've been muted.") - await user.move_to(None, reason=reason) + await user.add_roles(self._muted_role, reason=reason) + + log.trace(f"Attempting to kick {user} from voice because they've been muted.") + await user.move_to(None, reason=reason) await self.apply_infraction(ctx, infraction, user, action()) -- cgit v1.2.3 From 8a2865651556a598b5e96447c6ed4231829c46cf Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 2 Dec 2020 17:55:33 +0200 Subject: Merge NotFound caching with HttpException caching with status code --- bot/exts/moderation/infraction/_scheduler.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 6efa5b1e0..5726a5879 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -76,11 +76,9 @@ class InfractionScheduler: # Allowing mod log since this is a passive action that should be logged. try: await apply_coro - except discord.NotFound: - # When user joined and then right after this left again before action completed, this can't add roles - log.info(f"Can't reapply {infraction['type']} to user {infraction['user']} because user left again.") except discord.HTTPException as e: - if e.code == 10007: + # When user joined and then right after this left again before action completed, this can't apply roles + if e.code == 10007 or e.status == 404: log.info(f"Can't reapply {infraction['type']} to user {infraction['user']} because user left again.") else: log.warning( @@ -170,8 +168,6 @@ class InfractionScheduler: if expiry: # Schedule the expiration of the infraction. self.schedule_expiration(infraction) - except discord.NotFound: - log.info(f"Can't apply {infraction['type']} to user {infraction['user']} because user left from guild.") except discord.HTTPException as e: # Accordingly display that applying the infraction failed. # Don't use ctx.message.author; antispam only patches ctx.author. @@ -183,7 +179,7 @@ class InfractionScheduler: log_msg = f"Failed to apply {' '.join(infr_type.split('_'))} infraction #{id_} to {user}" if isinstance(e, discord.Forbidden): log.warning(f"{log_msg}: bot lacks permissions.") - elif e.code == 10007: + elif e.code == 10007 or e.status == 404: log.info( f"Can't apply {infraction['type']} to user {infraction['user']} because user left from guild." ) @@ -358,10 +354,8 @@ class InfractionScheduler: log.warning(f"Failed to deactivate infraction #{id_} ({type_}): bot lacks permissions.") log_text["Failure"] = "The bot lacks permissions to do this (role hierarchy?)" log_content = mod_role.mention - except discord.NotFound: - log.info(f"Can't pardon {infraction['type']} for user {infraction['user']} because user left from guild.") except discord.HTTPException as e: - if e.code == 10007: + if e.code == 10007 or e.status == 404: log.info( f"Can't pardon {infraction['type']} for user {infraction['user']} because user left from guild." ) -- cgit v1.2.3 From 50db55dd25f065222213510188e62b0d951b95c8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 2 Dec 2020 17:57:00 +0200 Subject: Fix user leaving from guild log grammar --- bot/exts/moderation/infraction/_scheduler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 5726a5879..835f3a2e1 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -79,7 +79,9 @@ class InfractionScheduler: except discord.HTTPException as e: # When user joined and then right after this left again before action completed, this can't apply roles if e.code == 10007 or e.status == 404: - log.info(f"Can't reapply {infraction['type']} to user {infraction['user']} because user left again.") + log.info( + f"Can't reapply {infraction['type']} to user {infraction['user']} because user left the guild." + ) else: log.warning( ( @@ -357,7 +359,7 @@ class InfractionScheduler: except discord.HTTPException as e: if e.code == 10007 or e.status == 404: log.info( - f"Can't pardon {infraction['type']} for user {infraction['user']} because user left from guild." + f"Can't pardon {infraction['type']} for user {infraction['user']} because user left the guild." ) else: log.exception(f"Failed to deactivate infraction #{id_} ({type_})") -- cgit v1.2.3 From 7c2ceede521fd0599b0fa1e55b8485008d80e08e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 2 Dec 2020 17:57:52 +0200 Subject: Log exception instead warning for unexpected HttpException --- 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 835f3a2e1..22739d332 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -83,7 +83,7 @@ class InfractionScheduler: f"Can't reapply {infraction['type']} to user {infraction['user']} because user left the guild." ) else: - log.warning( + log.exception( ( f"Got unexpected HTTPException (HTTP {e.status}, Discord code {e.code})" f"when awaiting {infraction['type']} coroutine for {infraction['user']}." -- cgit v1.2.3 From fc5930775ee2ae33ba88264a08c10b83761a8781 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 2 Dec 2020 17:58:25 +0200 Subject: Remove second unnecessary parenthesis --- bot/exts/moderation/infraction/_scheduler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 22739d332..8a45692d5 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -84,10 +84,8 @@ class InfractionScheduler: ) else: log.exception( - ( - f"Got unexpected HTTPException (HTTP {e.status}, Discord code {e.code})" - f"when awaiting {infraction['type']} coroutine for {infraction['user']}." - ) + f"Got unexpected HTTPException (HTTP {e.status}, Discord code {e.code})" + f"when awaiting {infraction['type']} coroutine for {infraction['user']}." ) else: log.info(f"Re-applied {infraction['type']} to user {infraction['user']} upon rejoining.") -- cgit v1.2.3 From eb73d3030d6f1d1aaf16defee9992f6336321f64 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 2 Dec 2020 18:00:34 +0200 Subject: Add failure message when applying infraction fails because user left --- bot/exts/moderation/infraction/_scheduler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 8a45692d5..ca4d18c98 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -359,6 +359,8 @@ class InfractionScheduler: log.info( f"Can't pardon {infraction['type']} for user {infraction['user']} because user left the guild." ) + log_text["Failure"] = f"User left the guild." + log_content = mod_role.mention else: log.exception(f"Failed to deactivate infraction #{id_} ({type_})") log_text["Failure"] = f"HTTPException with status {e.status} and code {e.code}." -- cgit v1.2.3 From 690ccd246e12d18a8c804b0802772f4a66a96bb8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 2 Dec 2020 18:43:22 +0200 Subject: Fix removing extensions and cogs for bot shutdown --- bot/bot.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index cdb4e72a9..06b1bd6e0 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -175,9 +175,13 @@ class Bot(commands.Bot): async def close(self) -> None: """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" # Done before super().close() to allow tasks finish before the HTTP session closes. - with suppress(Exception): - [self.unload_extension(ext) for ext in tuple(self.extensions)] - [self.remove_cog(cog) for cog in tuple(self.cogs)] + for ext in list(self.extensions): + with suppress(Exception): + self.unload_extension(ext) + + for cog in list(self.cogs): + with suppress(Exception): + self.remove_cog(cog) # Wait until all tasks that have to be completed before bot is closing is done log.trace("Waiting for tasks before closing.") -- cgit v1.2.3 From 00ff5738ea29d51d2db4c633a112da0b1a71aedd Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 2 Dec 2020 18:49:02 +0200 Subject: Remove unnecessary f-string --- 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 ca4d18c98..44c31cd13 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -359,7 +359,7 @@ class InfractionScheduler: log.info( f"Can't pardon {infraction['type']} for user {infraction['user']} because user left the guild." ) - log_text["Failure"] = f"User left the guild." + log_text["Failure"] = "User left the guild." log_content = mod_role.mention else: log.exception(f"Failed to deactivate infraction #{id_} ({type_})") -- cgit v1.2.3 From 9d96d490e33861bc037e693d0d8f885c05f28fc2 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Thu, 17 Dec 2020 17:53:15 +0200 Subject: Log info instead error for watchchannel consume task cancel --- 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 8894762f3..f9fc12dc3 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -347,7 +347,7 @@ class WatchChannel(metaclass=CogABCMeta): try: task.result() except asyncio.CancelledError: - self.log.error( + self.log.info( f"The consume task of {type(self).__name__} was canceled. Messages may be lost." ) -- cgit v1.2.3 From fc1f7ac9747a747f902a16de4cd6865c5b394568 Mon Sep 17 00:00:00 2001 From: Steele Date: Thu, 17 Dec 2020 22:19:42 -0500 Subject: User gets the bot DM when verified via `!verify`. `ALTERNATE_VERIFIED_MESSAGE` now begins "You're now verified!" instead of "Thanks for accepting our rules!". --- bot/exts/moderation/verification.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index 7aa559617..c413d36cf 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -55,7 +55,7 @@ If you'd like to unsubscribe from the announcement notifications, simply send `! """ ALTERNATE_VERIFIED_MESSAGE = f""" -Thanks for accepting our rules! +You're now verified! You can find a copy of our rules for reference at . @@ -861,6 +861,7 @@ class Verification(Cog): return await user.add_roles(developer_role) + await safe_dm(user.send(ALTERNATE_VERIFIED_MESSAGE)) log.trace(f'Developer role successfully applied to {user.id}') await ctx.send(f'{constants.Emojis.check_mark} Developer role applied to {user}.') -- cgit v1.2.3 From 2b09d739074f6d1ae259e234ea2ab787711d839d Mon Sep 17 00:00:00 2001 From: Steele Date: Thu, 17 Dec 2020 22:26:06 -0500 Subject: Responses from the bot mention the user. Previously, responses from the bot would say the name of the user rather than mentioning them. --- bot/exts/moderation/verification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index c413d36cf..8985a932f 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -857,13 +857,13 @@ class Verification(Cog): if developer_role in user.roles: log.trace(f'{user.id} is already a developer, aborting.') - await ctx.send(f'{constants.Emojis.cross_mark} {user} is already a developer.') + await ctx.send(f'{constants.Emojis.cross_mark} {user.mention} is already a developer.') return await user.add_roles(developer_role) await safe_dm(user.send(ALTERNATE_VERIFIED_MESSAGE)) log.trace(f'Developer role successfully applied to {user.id}') - await ctx.send(f'{constants.Emojis.check_mark} Developer role applied to {user}.') + await ctx.send(f'{constants.Emojis.check_mark} Developer role applied to {user.mention}.') # endregion -- cgit v1.2.3 From 8f425fc4ab1af10c4d601ebe755cdedf2fa2a0fd Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 19 Dec 2020 15:33:58 +0200 Subject: Pump Sentry SDK version from 0.14 to 0.19 --- Pipfile | 2 +- Pipfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Pipfile b/Pipfile index 23422869d..3ff653749 100644 --- a/Pipfile +++ b/Pipfile @@ -23,7 +23,7 @@ more_itertools = "~=8.2" python-dateutil = "~=2.8" pyyaml = "~=5.1" requests = "~=2.22" -sentry-sdk = "~=0.14" +sentry-sdk = "~=0.19" sphinx = "~=2.2" statsd = "~=3.3" emoji = "~=0.6" diff --git a/Pipfile.lock b/Pipfile.lock index ca72fb0f3..085d3d829 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bfaf61339c0cebb10d76f2e14ff967030008b8512b5f0b4c23c9e8997aab4552" + "sha256": "1ba637e521c654a23bcc82950e155f5366219eae00bbf809170a371122961a4f" }, "pipfile-spec": 6, "requires": { @@ -957,11 +957,11 @@ }, "flake8-tidy-imports": { "hashes": [ - "sha256:2821c79e83c656652d5ac6d3650ca370ed3c9752edb5383b1d50dee5bd8a383f", - "sha256:6cdd51e0d2f221e43ff4d5ac6331b1d95bbf4a5408906e36da913acaaed890e0" + "sha256:52e5f2f987d3d5597538d5941153409ebcab571635835b78f522c7bf03ca23bc", + "sha256:76e36fbbfdc8e3c5017f9a216c2855a298be85bc0631e66777f4e6a07a859dc4" ], "index": "pypi", - "version": "==4.2.0" + "version": "==4.2.1" }, "flake8-todo": { "hashes": [ -- cgit v1.2.3 From 4ebbb5db954fb82a66341cbae13ce6db7641f891 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 19 Dec 2020 15:38:15 +0200 Subject: Create workflow for creating Sentry release --- .github/workflows/sentry_release.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/sentry_release.yml diff --git a/.github/workflows/sentry_release.yml b/.github/workflows/sentry_release.yml new file mode 100644 index 000000000..b0e6876f8 --- /dev/null +++ b/.github/workflows/sentry_release.yml @@ -0,0 +1,24 @@ +name: Create Sentry release + +on: + push: + branches: + - master + +jobs: + create_sentry_release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@master + + - name: Create a Sentry.io release + uses: tclindner/sentry-releases-action@v1.2.0 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: python-discord + SENTRY_PROJECT: bot + with: + tagName: ${{ github.sha }} + environment: production + releaseNamePrefix: pydis-bot@ -- cgit v1.2.3 From d744dc779cd7c3b128cf28830f54a0dc2aecc574 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 19 Dec 2020 10:45:33 -0800 Subject: APIClient: simplify session creation Making the class-reusable is not worth the added complexity. --- bot/api.py | 42 ++++++------------------------------------ bot/bot.py | 7 ++++--- 2 files changed, 10 insertions(+), 39 deletions(-) diff --git a/bot/api.py b/bot/api.py index 4b8520582..6436c2b8b 100644 --- a/bot/api.py +++ b/bot/api.py @@ -50,52 +50,22 @@ class APIClient: self.session = None self.loop = loop - self._ready = asyncio.Event(loop=loop) - self._creation_task = None - self._default_session_kwargs = kwargs - - self.recreate() + # It has to be instantiated in a task/coroutine to avoid warnings from aiohttp. + self._creation_task = self.loop.create_task(self._create_session(**kwargs)) @staticmethod def _url_for(endpoint: str) -> str: return f"{URLs.site_schema}{URLs.site_api}/{quote_url(endpoint)}" async def _create_session(self, **session_kwargs) -> None: - """ - Create the aiohttp session with `session_kwargs` and set the ready event. - - `session_kwargs` is merged with `_default_session_kwargs` and overwrites its values. - If an open session already exists, it will first be closed. - """ - await self.close() - self.session = aiohttp.ClientSession(**{**self._default_session_kwargs, **session_kwargs}) - self._ready.set() + """Create the aiohttp session with `session_kwargs`.""" + self.session = aiohttp.ClientSession(**session_kwargs) async def close(self) -> None: """Close the aiohttp session and unset the ready event.""" if self.session: await self.session.close() - self._ready.clear() - - def recreate(self, force: bool = False, **session_kwargs) -> None: - """ - Schedule the aiohttp session to be created with `session_kwargs` if it's been closed. - - If `force` is True, the session will be recreated even if an open one exists. If a task to - create the session is pending, it will be cancelled. - - `session_kwargs` is merged with the kwargs given when the `APIClient` was created and - overwrites those default kwargs. - """ - if force or self.session is None or self.session.closed: - if force and self._creation_task: - self._creation_task.cancel() - - # Don't schedule a task if one is already in progress. - if force or self._creation_task is None or self._creation_task.done(): - self._creation_task = self.loop.create_task(self._create_session(**session_kwargs)) - async def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool) -> None: """Raise ResponseCodeError for non-OK response if an exception should be raised.""" if should_raise and response.status >= 400: @@ -108,7 +78,7 @@ class APIClient: async def request(self, method: str, endpoint: str, *, raise_for_status: bool = True, **kwargs) -> dict: """Send an HTTP request to the site API and return the JSON response.""" - await self._ready.wait() + await self._creation_task async with self.session.request(method.upper(), self._url_for(endpoint), **kwargs) as resp: await self.maybe_raise_for_status(resp, raise_for_status) @@ -132,7 +102,7 @@ class APIClient: async def delete(self, endpoint: str, *, raise_for_status: bool = True, **kwargs) -> Optional[dict]: """Site API DELETE.""" - await self._ready.wait() + await self._creation_task async with self.session.delete(self._url_for(endpoint), **kwargs) as resp: if resp.status == 204: diff --git a/bot/bot.py b/bot/bot.py index f71f5d1fb..1715f3ca3 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -32,7 +32,7 @@ class Bot(commands.Bot): self.http_session: Optional[aiohttp.ClientSession] = None self.redis_session = redis_session - self.api_client = api.APIClient(loop=self.loop) + self.api_client = None self.filter_list_cache = defaultdict(dict) self._connector = None @@ -112,7 +112,7 @@ class Bot(commands.Bot): ) self.http_session = aiohttp.ClientSession(connector=self._connector) - self.api_client.recreate(force=True, connector=self._connector) + self.api_client = api.APIClient(loop=self.loop, connector=self._connector) # Build the FilterList cache self.loop.create_task(self.cache_filter_list_data()) @@ -194,7 +194,8 @@ class Bot(commands.Bot): """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" await super().close() - await self.api_client.close() + if self.api_client: + await self.api_client.close() if self.http_session: await self.http_session.close() -- cgit v1.2.3 From 2021c78635de4ae5a9f9835944c87cba699b41c0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 19 Dec 2020 10:46:26 -0800 Subject: APIClient: remove obsolete function --- bot/api.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/bot/api.py b/bot/api.py index 6436c2b8b..ce3f2ada3 100644 --- a/bot/api.py +++ b/bot/api.py @@ -110,17 +110,3 @@ class APIClient: await self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() - - -def loop_is_running() -> bool: - """ - Determine if there is a running asyncio event loop. - - This helps enable "call this when event loop is running" logic (see: Twisted's `callWhenRunning`), - which is currently not provided by asyncio. - """ - try: - asyncio.get_running_loop() - except RuntimeError: - return False - return True -- cgit v1.2.3 From 598c4a50c0c4f2e0c2139d6c349ee183010dbf78 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 19 Dec 2020 10:59:09 -0800 Subject: Bot: cease support of Bot.clear() Supporting the function means supporting re-use of a closed Bot. However, this functionality is not relied upon by anything nor will it ever be in the foreseeable future. Support of it required scheduling any needed startup coroutines as tasks. This made augmenting the Bot clunky and didn't make it easy to wait for startup coroutines to complete before logging in. --- bot/bot.py | 79 ++++++++++++++++++++++---------------------------------------- 1 file changed, 28 insertions(+), 51 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index 1715f3ca3..b01cbea43 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -32,7 +32,7 @@ class Bot(commands.Bot): self.http_session: Optional[aiohttp.ClientSession] = None self.redis_session = redis_session - self.api_client = None + self.api_client: Optional[api.APIClient] = None self.filter_list_cache = defaultdict(dict) self._connector = None @@ -77,46 +77,6 @@ class Bot(commands.Bot): for item in full_cache: self.insert_item_into_filter_list_cache(item) - def _recreate(self) -> None: - """Re-create the connector, aiohttp session, the APIClient and the Redis session.""" - # Use asyncio for DNS resolution instead of threads so threads aren't spammed. - # Doesn't seem to have any state with regards to being closed, so no need to worry? - self._resolver = aiohttp.AsyncResolver() - - # Its __del__ does send a warning but it doesn't always show up for some reason. - if self._connector and not self._connector._closed: - log.warning( - "The previous connector was not closed; it will remain open and be overwritten" - ) - - if self.redis_session.closed: - # If the RedisSession was somehow closed, we try to reconnect it - # here. Normally, this shouldn't happen. - self.loop.create_task(self.redis_session.connect()) - - # Use AF_INET as its socket family to prevent HTTPS related problems both locally - # and in production. - self._connector = aiohttp.TCPConnector( - resolver=self._resolver, - family=socket.AF_INET, - ) - - # Client.login() will call HTTPClient.static_login() which will create a session using - # this connector attribute. - self.http.connector = self._connector - - # Its __del__ does send a warning but it doesn't always show up for some reason. - if self.http_session and not self.http_session.closed: - log.warning( - "The previous session was not closed; it will remain open and be overwritten" - ) - - self.http_session = aiohttp.ClientSession(connector=self._connector) - self.api_client = api.APIClient(loop=self.loop, connector=self._connector) - - # Build the FilterList cache - self.loop.create_task(self.cache_filter_list_data()) - @classmethod def create(cls) -> "Bot": """Create and return an instance of a Bot.""" @@ -180,15 +140,8 @@ class Bot(commands.Bot): return command def clear(self) -> None: - """ - Clears the internal state of the bot and recreates the connector and sessions. - - Will cause a DeprecationWarning if called outside a coroutine. - """ - # Because discord.py recreates the HTTPClient session, may as well follow suit and recreate - # our own stuff here too. - self._recreate() - super().clear() + """Not implemented! Re-instantiate the bot instead of attempting to re-use a closed one.""" + raise NotImplementedError("Re-using a Bot object after closing it is not supported.") async def close(self) -> None: """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" @@ -230,7 +183,31 @@ class Bot(commands.Bot): async def login(self, *args, **kwargs) -> None: """Re-create the connector and set up sessions before logging into Discord.""" - self._recreate() + # Use asyncio for DNS resolution instead of threads so threads aren't spammed. + self._resolver = aiohttp.AsyncResolver() + + # Use AF_INET as its socket family to prevent HTTPS related problems both locally + # and in production. + self._connector = aiohttp.TCPConnector( + resolver=self._resolver, + family=socket.AF_INET, + ) + + # Client.login() will call HTTPClient.static_login() which will create a session using + # this connector attribute. + self.http.connector = self._connector + + self.http_session = aiohttp.ClientSession(connector=self._connector) + self.api_client = api.APIClient(loop=self.loop, connector=self._connector) + + if self.redis_session.closed: + # If the RedisSession was somehow closed, we try to reconnect it + # here. Normally, this shouldn't happen. + await self.redis_session.connect() + + # Build the FilterList cache + await self.cache_filter_list_data() + await self.stats.create_socket() await super().login(*args, **kwargs) -- cgit v1.2.3 From c58fbe828781444fc282151c06c2f4fb9057fe59 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 19 Dec 2020 11:29:05 -0800 Subject: APIClient: create the session directly in __init__ The client is already instantiated in a coroutine and aiohttp won't complain. Therefore, scheduling a task to create the session is redundant. --- bot/api.py | 29 +++++++++-------------------- bot/bot.py | 2 +- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/bot/api.py b/bot/api.py index ce3f2ada3..d93f9f2ba 100644 --- a/bot/api.py +++ b/bot/api.py @@ -37,34 +37,27 @@ class APIClient: session: Optional[aiohttp.ClientSession] = None loop: asyncio.AbstractEventLoop = None - def __init__(self, loop: asyncio.AbstractEventLoop, **kwargs): + def __init__(self, **session_kwargs): auth_headers = { 'Authorization': f"Token {Keys.site_api}" } - if 'headers' in kwargs: - kwargs['headers'].update(auth_headers) + if 'headers' in session_kwargs: + session_kwargs['headers'].update(auth_headers) else: - kwargs['headers'] = auth_headers + session_kwargs['headers'] = auth_headers - self.session = None - self.loop = loop - - # It has to be instantiated in a task/coroutine to avoid warnings from aiohttp. - self._creation_task = self.loop.create_task(self._create_session(**kwargs)) + # aiohttp will complain if APIClient gets instantiated outside a coroutine. Thankfully, we + # don't and shouldn't need to do that, so we can avoid scheduling a task to create it. + self.session = aiohttp.ClientSession(**session_kwargs) @staticmethod def _url_for(endpoint: str) -> str: return f"{URLs.site_schema}{URLs.site_api}/{quote_url(endpoint)}" - async def _create_session(self, **session_kwargs) -> None: - """Create the aiohttp session with `session_kwargs`.""" - self.session = aiohttp.ClientSession(**session_kwargs) - async def close(self) -> None: - """Close the aiohttp session and unset the ready event.""" - if self.session: - await self.session.close() + """Close the aiohttp session.""" + await self.session.close() async def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool) -> None: """Raise ResponseCodeError for non-OK response if an exception should be raised.""" @@ -78,8 +71,6 @@ class APIClient: async def request(self, method: str, endpoint: str, *, raise_for_status: bool = True, **kwargs) -> dict: """Send an HTTP request to the site API and return the JSON response.""" - await self._creation_task - async with self.session.request(method.upper(), self._url_for(endpoint), **kwargs) as resp: await self.maybe_raise_for_status(resp, raise_for_status) return await resp.json() @@ -102,8 +93,6 @@ class APIClient: async def delete(self, endpoint: str, *, raise_for_status: bool = True, **kwargs) -> Optional[dict]: """Site API DELETE.""" - await self._creation_task - async with self.session.delete(self._url_for(endpoint), **kwargs) as resp: if resp.status == 204: return None diff --git a/bot/bot.py b/bot/bot.py index b01cbea43..4ebe0a5c3 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -198,7 +198,7 @@ class Bot(commands.Bot): self.http.connector = self._connector self.http_session = aiohttp.ClientSession(connector=self._connector) - self.api_client = api.APIClient(loop=self.loop, connector=self._connector) + self.api_client = api.APIClient(connector=self._connector) if self.redis_session.closed: # If the RedisSession was somehow closed, we try to reconnect it -- cgit v1.2.3 From 52ee0dab1e59089a7acc9f08078dee0df3fa40e6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 19 Dec 2020 11:33:37 -0800 Subject: Remove obsolete test cases Forgot to remove these when removing `loop_is_running` in a previous commit. --- tests/bot/test_api.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/bot/test_api.py b/tests/bot/test_api.py index 99e942813..76bcb481d 100644 --- a/tests/bot/test_api.py +++ b/tests/bot/test_api.py @@ -13,14 +13,6 @@ class APIClientTests(unittest.IsolatedAsyncioTestCase): cls.error_api_response = MagicMock() cls.error_api_response.status = 999 - def test_loop_is_not_running_by_default(self): - """The event loop should not be running by default.""" - self.assertFalse(api.loop_is_running()) - - async def test_loop_is_running_in_async_context(self): - """The event loop should be running in an async context.""" - self.assertTrue(api.loop_is_running()) - def test_response_code_error_default_initialization(self): """Test the default initialization of `ResponseCodeError` without `text` or `json`""" error = api.ResponseCodeError(response=self.error_api_response) -- cgit v1.2.3 From 756fa37d3727e6448dd352d767b0e52c79691d1c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 08:58:55 +0200 Subject: Consume Git SHA build arg and add to it to environment --- Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0b1674e7a..5d0380b44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,14 @@ FROM python:3.8-slim +# Define Git SHA build argument +ARG git_sha="development" + # Set pip to have cleaner logs and no saved cache ENV PIP_NO_CACHE_DIR=false \ PIPENV_HIDE_EMOJIS=1 \ PIPENV_IGNORE_VIRTUALENVS=1 \ - PIPENV_NOSPIN=1 + PIPENV_NOSPIN=1 \ + GIT_SHA=$git_sha RUN apt-get -y update \ && apt-get install -y \ -- cgit v1.2.3 From 8da34233fe7d129ee199efdaa92ea9fc67a0e35f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 09:00:32 +0200 Subject: Inject Git SHA in container build workflow --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6152f1543..25bcce848 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,3 +55,5 @@ jobs: tags: | ghcr.io/python-discord/bot:latest ghcr.io/python-discord/bot:${{ steps.sha_tag.outputs.tag }} + build-args: | + git_sha=${{ GITHUB_SHA }} -- cgit v1.2.3 From 33ad294ae9fd1128982c6ccd7e183e5b2dfc4752 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 09:00:45 +0200 Subject: Add constant for Git SHA --- bot/constants.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index c4bb6b2d6..92287a930 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -656,6 +656,9 @@ MODERATION_CHANNELS = Guild.moderation_channels # Category combinations MODERATION_CATEGORIES = Guild.moderation_categories +# Git SHA for Sentry +GIT_SHA = os.environ.get("GIT_SHA", "development") + # Bot replies NEGATIVE_REPLIES = [ "Noooooo!!", -- cgit v1.2.3 From 4c7ada8ee41f097e2b9b753114fadca28998da88 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 09:02:16 +0200 Subject: Attach release on Sentry SDK initialization --- bot/log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/log.py b/bot/log.py index 13141de40..bb44cee8a 100644 --- a/bot/log.py +++ b/bot/log.py @@ -69,7 +69,8 @@ def setup_sentry() -> None: sentry_logging, AioHttpIntegration(), RedisIntegration(), - ] + ], + release=f"bot@{constants.GIT_SHA}" ) -- cgit v1.2.3 From e7ca3af18b92c732fac8f688df17da614457cd54 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 09:03:34 +0200 Subject: Use bot prefix instead pydis-bot for Sentry release workflow --- .github/workflows/sentry_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sentry_release.yml b/.github/workflows/sentry_release.yml index b0e6876f8..b8d92e90a 100644 --- a/.github/workflows/sentry_release.yml +++ b/.github/workflows/sentry_release.yml @@ -21,4 +21,4 @@ jobs: with: tagName: ${{ github.sha }} environment: production - releaseNamePrefix: pydis-bot@ + releaseNamePrefix: bot@ -- cgit v1.2.3 From 981eac1427ce7fd8d16bf79bfef68af86b625b10 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 09:05:02 +0200 Subject: Remove aiohttp integration --- bot/log.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/log.py b/bot/log.py index bb44cee8a..0935666d1 100644 --- a/bot/log.py +++ b/bot/log.py @@ -6,7 +6,6 @@ from pathlib import Path import coloredlogs import sentry_sdk -from sentry_sdk.integrations.aiohttp import AioHttpIntegration from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.redis import RedisIntegration @@ -67,7 +66,6 @@ def setup_sentry() -> None: dsn=constants.Bot.sentry_dsn, integrations=[ sentry_logging, - AioHttpIntegration(), RedisIntegration(), ], release=f"bot@{constants.GIT_SHA}" -- cgit v1.2.3 From 477af4efe7a0ed155bf6f5805a2d0fd3674e0e6f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 09:38:49 +0200 Subject: Add GitHub API key to config as environment variable --- bot/constants.py | 1 + config-default.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index c4bb6b2d6..25a4c4d09 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -493,6 +493,7 @@ class Keys(metaclass=YAMLGetter): section = "keys" site_api: Optional[str] + github: Optional[str] class URLs(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index 3f3f66962..ca89bb639 100644 --- a/config-default.yml +++ b/config-default.yml @@ -323,6 +323,7 @@ filter: keys: site_api: !ENV "BOT_API_KEY" + github: !ENV "GITHUB_API_KEY" urls: -- cgit v1.2.3 From de3dd22f3ee6b219ecd1569c56cdd480eadde298 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 09:59:56 +0200 Subject: Move PEP related functions and command to own cog --- bot/exts/utils/pep.py | 153 ++++++++++++++++++++++++++++++++++++++++++++++++ bot/exts/utils/utils.py | 137 +------------------------------------------ 2 files changed, 154 insertions(+), 136 deletions(-) create mode 100644 bot/exts/utils/pep.py diff --git a/bot/exts/utils/pep.py b/bot/exts/utils/pep.py new file mode 100644 index 000000000..71c710087 --- /dev/null +++ b/bot/exts/utils/pep.py @@ -0,0 +1,153 @@ +import logging +from datetime import datetime, timedelta +from email.parser import HeaderParser +from io import StringIO +from typing import Dict, Optional, Tuple + +from discord import Colour, Embed +from discord.ext.commands import Cog, Context, command + +from bot.bot import Bot +from bot.utils.cache import AsyncCache + +log = logging.getLogger(__name__) + +ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" + +pep_cache = AsyncCache() + + +class PythonEnhancementProposals(Cog): + """Cog for displaying information about PEPs.""" + + BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" + BASE_GITHUB_PEP_URL = "https://raw.githubusercontent.com/python/peps/master/pep-" + PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" + + def __init__(self, bot: Bot): + self.bot = bot + self.peps: Dict[int, str] = {} + self.last_refreshed_peps: Optional[datetime] = None + self.bot.loop.create_task(self.refresh_peps_urls()) + + async def refresh_peps_urls(self) -> None: + """Refresh PEP URLs listing in every 3 hours.""" + # Wait until HTTP client is available + await self.bot.wait_until_ready() + log.trace("Started refreshing PEP URLs.") + + async with self.bot.http_session.get(self.PEPS_LISTING_API_URL) as resp: + listing = await resp.json() + + log.trace("Got PEP URLs listing from GitHub API") + + for file in listing: + name = file["name"] + if name.startswith("pep-") and name.endswith((".rst", ".txt")): + pep_number = name.replace("pep-", "").split(".")[0] + self.peps[int(pep_number)] = file["download_url"] + + self.last_refreshed_peps = datetime.now() + log.info("Successfully refreshed PEP URLs listing.") + + @staticmethod + def get_pep_zero_embed() -> Embed: + """Get information embed about PEP 0.""" + pep_embed = Embed( + title="**PEP 0 - Index of Python Enhancement Proposals (PEPs)**", + url="https://www.python.org/dev/peps/" + ) + pep_embed.set_thumbnail(url=ICON_URL) + pep_embed.add_field(name="Status", value="Active") + pep_embed.add_field(name="Created", value="13-Jul-2000") + pep_embed.add_field(name="Type", value="Informational") + + return pep_embed + + async def validate_pep_number(self, pep_nr: int) -> Optional[Embed]: + """Validate is PEP number valid. When it isn't, return error embed, otherwise None.""" + if ( + pep_nr not in self.peps + and (self.last_refreshed_peps + timedelta(minutes=30)) <= datetime.now() + and len(str(pep_nr)) < 5 + ): + await self.refresh_peps_urls() + + if pep_nr not in self.peps: + log.trace(f"PEP {pep_nr} was not found") + return Embed( + title="PEP not found", + description=f"PEP {pep_nr} does not exist.", + colour=Colour.red() + ) + + return None + + def generate_pep_embed(self, pep_header: Dict, pep_nr: int) -> Embed: + """Generate PEP embed based on PEP headers data.""" + # Assemble the embed + pep_embed = Embed( + title=f"**PEP {pep_nr} - {pep_header['Title']}**", + description=f"[Link]({self.BASE_PEP_URL}{pep_nr:04})", + ) + + pep_embed.set_thumbnail(url=ICON_URL) + + # Add the interesting information + fields_to_check = ("Status", "Python-Version", "Created", "Type") + for field in fields_to_check: + # Check for a PEP metadata field that is present but has an empty value + # embed field values can't contain an empty string + if pep_header.get(field, ""): + pep_embed.add_field(name=field, value=pep_header[field]) + + return pep_embed + + @pep_cache(arg_offset=1) + async def get_pep_embed(self, pep_nr: int) -> Tuple[Embed, bool]: + """Fetch, generate and return PEP embed. Second item of return tuple show does getting success.""" + response = await self.bot.http_session.get(self.peps[pep_nr]) + + if response.status == 200: + log.trace(f"PEP {pep_nr} found") + pep_content = await response.text() + + # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179 + pep_header = HeaderParser().parse(StringIO(pep_content)) + return self.generate_pep_embed(pep_header, pep_nr), True + else: + log.trace( + f"The user requested PEP {pep_nr}, but the response had an unexpected status code: {response.status}." + ) + return Embed( + title="Unexpected error", + description="Unexpected HTTP error during PEP search. Please let us know.", + colour=Colour.red() + ), False + + @command(name='pep', aliases=('get_pep', 'p')) + async def pep_command(self, ctx: Context, pep_number: int) -> None: + """Fetches information about a PEP and sends it to the channel.""" + # Trigger typing in chat to show users that bot is responding + await ctx.trigger_typing() + + # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs. + if pep_number == 0: + pep_embed = self.get_pep_zero_embed() + success = True + else: + success = False + if not (pep_embed := await self.validate_pep_number(pep_number)): + pep_embed, success = await self.get_pep_embed(pep_number) + + await ctx.send(embed=pep_embed) + if success: + log.trace(f"PEP {pep_number} getting and sending finished successfully. Increasing stat.") + self.bot.stats.incr(f"pep_fetches.{pep_number}") + else: + log.trace(f"Getting PEP {pep_number} failed. Error embed sent.") + + +def setup(bot: Bot) -> None: + """Load the PEP cog.""" + bot.add_cog(PythonEnhancementProposals(bot)) diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 8e7e6ba36..eb92dfca7 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -2,10 +2,7 @@ import difflib import logging import re import unicodedata -from datetime import datetime, timedelta -from email.parser import HeaderParser -from io import StringIO -from typing import Dict, Optional, Tuple, Union +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 @@ -17,7 +14,6 @@ from bot.converters import Snowflake from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils import messages -from bot.utils.cache import AsyncCache from bot.utils.time import time_since log = logging.getLogger(__name__) @@ -44,23 +40,12 @@ If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those! """ -ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" - -pep_cache = AsyncCache() - class Utils(Cog): """A selection of utilities which don't have a clear category.""" - BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" - BASE_GITHUB_PEP_URL = "https://raw.githubusercontent.com/python/peps/master/pep-" - PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" - def __init__(self, bot: Bot): self.bot = bot - self.peps: Dict[int, str] = {} - self.last_refreshed_peps: Optional[datetime] = None - self.bot.loop.create_task(self.refresh_peps_urls()) @command() @in_whitelist(channels=(Channels.bot_commands,), roles=STAFF_ROLES) @@ -207,126 +192,6 @@ class Utils(Cog): for reaction in options: await message.add_reaction(reaction) - # region: PEP - - async def refresh_peps_urls(self) -> None: - """Refresh PEP URLs listing in every 3 hours.""" - # Wait until HTTP client is available - await self.bot.wait_until_ready() - log.trace("Started refreshing PEP URLs.") - - async with self.bot.http_session.get(self.PEPS_LISTING_API_URL) as resp: - listing = await resp.json() - - log.trace("Got PEP URLs listing from GitHub API") - - for file in listing: - name = file["name"] - if name.startswith("pep-") and name.endswith((".rst", ".txt")): - pep_number = name.replace("pep-", "").split(".")[0] - self.peps[int(pep_number)] = file["download_url"] - - self.last_refreshed_peps = datetime.now() - log.info("Successfully refreshed PEP URLs listing.") - - @command(name='pep', aliases=('get_pep', 'p')) - async def pep_command(self, ctx: Context, pep_number: int) -> None: - """Fetches information about a PEP and sends it to the channel.""" - # Trigger typing in chat to show users that bot is responding - await ctx.trigger_typing() - - # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs. - if pep_number == 0: - pep_embed = self.get_pep_zero_embed() - success = True - else: - success = False - if not (pep_embed := await self.validate_pep_number(pep_number)): - pep_embed, success = await self.get_pep_embed(pep_number) - - await ctx.send(embed=pep_embed) - if success: - log.trace(f"PEP {pep_number} getting and sending finished successfully. Increasing stat.") - self.bot.stats.incr(f"pep_fetches.{pep_number}") - else: - log.trace(f"Getting PEP {pep_number} failed. Error embed sent.") - - @staticmethod - def get_pep_zero_embed() -> Embed: - """Get information embed about PEP 0.""" - pep_embed = Embed( - title="**PEP 0 - Index of Python Enhancement Proposals (PEPs)**", - url="https://www.python.org/dev/peps/" - ) - pep_embed.set_thumbnail(url=ICON_URL) - pep_embed.add_field(name="Status", value="Active") - pep_embed.add_field(name="Created", value="13-Jul-2000") - pep_embed.add_field(name="Type", value="Informational") - - return pep_embed - - async def validate_pep_number(self, pep_nr: int) -> Optional[Embed]: - """Validate is PEP number valid. When it isn't, return error embed, otherwise None.""" - if ( - pep_nr not in self.peps - and (self.last_refreshed_peps + timedelta(minutes=30)) <= datetime.now() - and len(str(pep_nr)) < 5 - ): - await self.refresh_peps_urls() - - if pep_nr not in self.peps: - log.trace(f"PEP {pep_nr} was not found") - return Embed( - title="PEP not found", - description=f"PEP {pep_nr} does not exist.", - colour=Colour.red() - ) - - return None - - def generate_pep_embed(self, pep_header: Dict, pep_nr: int) -> Embed: - """Generate PEP embed based on PEP headers data.""" - # Assemble the embed - pep_embed = Embed( - title=f"**PEP {pep_nr} - {pep_header['Title']}**", - description=f"[Link]({self.BASE_PEP_URL}{pep_nr:04})", - ) - - pep_embed.set_thumbnail(url=ICON_URL) - - # Add the interesting information - fields_to_check = ("Status", "Python-Version", "Created", "Type") - for field in fields_to_check: - # Check for a PEP metadata field that is present but has an empty value - # embed field values can't contain an empty string - if pep_header.get(field, ""): - pep_embed.add_field(name=field, value=pep_header[field]) - - return pep_embed - - @pep_cache(arg_offset=1) - async def get_pep_embed(self, pep_nr: int) -> Tuple[Embed, bool]: - """Fetch, generate and return PEP embed. Second item of return tuple show does getting success.""" - response = await self.bot.http_session.get(self.peps[pep_nr]) - - if response.status == 200: - log.trace(f"PEP {pep_nr} found") - pep_content = await response.text() - - # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179 - pep_header = HeaderParser().parse(StringIO(pep_content)) - return self.generate_pep_embed(pep_header, pep_nr), True - else: - log.trace( - f"The user requested PEP {pep_nr}, but the response had an unexpected status code: {response.status}." - ) - return Embed( - title="Unexpected error", - description="Unexpected HTTP error during PEP search. Please let us know.", - colour=Colour.red() - ), False - # endregion - def setup(bot: Bot) -> None: """Load the Utils cog.""" -- cgit v1.2.3 From d9fed5807429bb8029b8d623abed67ee03d211a4 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 10:02:49 +0200 Subject: Set last PEPs listing at beginning of function --- bot/exts/utils/pep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/pep.py b/bot/exts/utils/pep.py index 71c710087..d60a40658 100644 --- a/bot/exts/utils/pep.py +++ b/bot/exts/utils/pep.py @@ -35,6 +35,7 @@ class PythonEnhancementProposals(Cog): # Wait until HTTP client is available await self.bot.wait_until_ready() log.trace("Started refreshing PEP URLs.") + self.last_refreshed_peps = datetime.now() async with self.bot.http_session.get(self.PEPS_LISTING_API_URL) as resp: listing = await resp.json() @@ -47,7 +48,6 @@ class PythonEnhancementProposals(Cog): pep_number = name.replace("pep-", "").split(".")[0] self.peps[int(pep_number)] = file["download_url"] - self.last_refreshed_peps = datetime.now() log.info("Successfully refreshed PEP URLs listing.") @staticmethod -- cgit v1.2.3 From b7ab1595fd4d9e8e9f8e0e2285fe4b4dfdc2674d Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 10:04:31 +0200 Subject: Make last PEPs listing refresh non-optional --- bot/exts/utils/pep.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/utils/pep.py b/bot/exts/utils/pep.py index d60a40658..e0b06d63e 100644 --- a/bot/exts/utils/pep.py +++ b/bot/exts/utils/pep.py @@ -27,7 +27,8 @@ class PythonEnhancementProposals(Cog): def __init__(self, bot: Bot): self.bot = bot self.peps: Dict[int, str] = {} - self.last_refreshed_peps: Optional[datetime] = None + # To avoid situations where we don't have last datetime, set this to now. + self.last_refreshed_peps: datetime = datetime.now() self.bot.loop.create_task(self.refresh_peps_urls()) async def refresh_peps_urls(self) -> None: -- cgit v1.2.3 From 4527f9a674a28952d1da670e934921d12cdc14b6 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 10:28:43 +0200 Subject: Log warning and return early when can't get PEP URLs from API --- bot/exts/utils/pep.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/exts/utils/pep.py b/bot/exts/utils/pep.py index e0b06d63e..d642c902a 100644 --- a/bot/exts/utils/pep.py +++ b/bot/exts/utils/pep.py @@ -39,6 +39,10 @@ class PythonEnhancementProposals(Cog): self.last_refreshed_peps = datetime.now() async with self.bot.http_session.get(self.PEPS_LISTING_API_URL) as resp: + if resp.status != 200: + log.warning(f"Fetching PEP URLs from GitHub API failed with code {resp.status}") + return + listing = await resp.json() log.trace("Got PEP URLs listing from GitHub API") -- cgit v1.2.3 From d55adafde54d6b695f6e2ba91c3813c45ea95d0e Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 10:31:43 +0200 Subject: Implement GitHub API authorization header --- bot/exts/utils/pep.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bot/exts/utils/pep.py b/bot/exts/utils/pep.py index d642c902a..df9ad2ba9 100644 --- a/bot/exts/utils/pep.py +++ b/bot/exts/utils/pep.py @@ -8,6 +8,7 @@ from discord import Colour, Embed from discord.ext.commands import Cog, Context, command from bot.bot import Bot +from bot.constants import Keys from bot.utils.cache import AsyncCache log = logging.getLogger(__name__) @@ -16,6 +17,10 @@ ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" pep_cache = AsyncCache() +GITHUB_API_HEADERS = {} +if Keys.github: + GITHUB_API_HEADERS["Authorization"] = f"token {Keys.github}" + class PythonEnhancementProposals(Cog): """Cog for displaying information about PEPs.""" @@ -38,7 +43,10 @@ class PythonEnhancementProposals(Cog): log.trace("Started refreshing PEP URLs.") self.last_refreshed_peps = datetime.now() - async with self.bot.http_session.get(self.PEPS_LISTING_API_URL) as resp: + async with self.bot.http_session.get( + self.PEPS_LISTING_API_URL, + headers=GITHUB_API_HEADERS + ) as resp: if resp.status != 200: log.warning(f"Fetching PEP URLs from GitHub API failed with code {resp.status}") return -- cgit v1.2.3 From 9870072310e5a2d1ccf6d5a035d1f1044343ff5f Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 10:35:05 +0200 Subject: Remove unused constant --- bot/exts/utils/pep.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/utils/pep.py b/bot/exts/utils/pep.py index df9ad2ba9..873f32a8e 100644 --- a/bot/exts/utils/pep.py +++ b/bot/exts/utils/pep.py @@ -26,7 +26,6 @@ class PythonEnhancementProposals(Cog): """Cog for displaying information about PEPs.""" BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" - BASE_GITHUB_PEP_URL = "https://raw.githubusercontent.com/python/peps/master/pep-" PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" def __init__(self, bot: Bot): -- cgit v1.2.3 From d5988993e71e6dcd78b454a29d7132fc11fb6e71 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sun, 20 Dec 2020 17:22:20 +0200 Subject: Fix wrong way for getting Git SHA --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25bcce848..6c97e8784 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,4 +56,4 @@ jobs: ghcr.io/python-discord/bot:latest ghcr.io/python-discord/bot:${{ steps.sha_tag.outputs.tag }} build-args: | - git_sha=${{ GITHUB_SHA }} + git_sha=${{ github.sha }} -- cgit v1.2.3 From 6115f5a9f9c4c72e3ec7cac02372f10135b836bc Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sun, 20 Dec 2020 16:56:27 +0100 Subject: Add the clear alias to the clean command --- bot/exts/utils/clean.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/clean.py b/bot/exts/utils/clean.py index bf25cb4c2..8acaf9131 100644 --- a/bot/exts/utils/clean.py +++ b/bot/exts/utils/clean.py @@ -191,7 +191,7 @@ class Clean(Cog): channel_id=Channels.mod_log, ) - @group(invoke_without_command=True, name="clean", aliases=["purge"]) + @group(invoke_without_command=True, name="clean", aliases=["clear", "purge"]) @has_any_role(*MODERATION_ROLES) async def clean_group(self, ctx: Context) -> None: """Commands for cleaning messages in channels.""" -- cgit v1.2.3 From ce46567546488f87f458b5d4fe1894d90e848044 Mon Sep 17 00:00:00 2001 From: Steele Date: Tue, 22 Dec 2020 20:30:08 -0500 Subject: Rewrite `!verify` to account for new native-gate-only verification. Renamed method; if not `user.pending`, adds and immediately removes an arbitrary role (namely the Announcements role), which verifies the user. --- bot/exts/moderation/verification.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index ca3e97e2e..dbd3c42a6 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -834,20 +834,21 @@ class Verification(Cog): @command(name='verify') @has_any_role(*constants.MODERATION_ROLES) - async def apply_developer_role(self, ctx: Context, user: discord.Member) -> None: - """Command for moderators to apply the Developer role to any user.""" + async def perform_manual_verification(self, ctx: Context, user: discord.Member) -> None: + """Command for moderators to verify any user.""" log.trace(f'verify command called by {ctx.author} for {user.id}.') - developer_role = self.bot.get_guild(constants.Guild.id).get_role(constants.Roles.verified) - if developer_role in user.roles: - log.trace(f'{user.id} is already a developer, aborting.') - await ctx.send(f'{constants.Emojis.cross_mark} {user.mention} is already a developer.') + if user.pending: + log.trace(f'{user.id} is already verified, aborting.') + await ctx.send(f'{constants.Emojis.cross_mark} {user.mention} is already verified.') return - await user.add_roles(developer_role) - await safe_dm(user.send(ALTERNATE_VERIFIED_MESSAGE)) - log.trace(f'Developer role successfully applied to {user.id}') - await ctx.send(f'{constants.Emojis.check_mark} Developer role applied to {user.mention}.') + # Adding a role automatically verifies the user, so we add and remove the Announcements role. + temporary_role = self.bot.get_guild(constants.Guild.id).get_role(constants.Roles.announcements) + await user.add_roles(temporary_role) + await user.remove_roles(temporary_role) + log.trace(f'{user.id} manually verified.') + await ctx.send(f'{constants.Emojis.check_mark} {user.mention} is now verified.') # endregion -- cgit v1.2.3 From dd546b8970f9643dd1ff4a2f09c8a675d6bec5a8 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 23 Dec 2020 17:31:03 +0200 Subject: Move constants out from class --- bot/exts/utils/pep.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/utils/pep.py b/bot/exts/utils/pep.py index 873f32a8e..8ac96bbdb 100644 --- a/bot/exts/utils/pep.py +++ b/bot/exts/utils/pep.py @@ -14,6 +14,8 @@ from bot.utils.cache import AsyncCache log = logging.getLogger(__name__) ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" +BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" +PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" pep_cache = AsyncCache() @@ -25,9 +27,6 @@ if Keys.github: class PythonEnhancementProposals(Cog): """Cog for displaying information about PEPs.""" - BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" - PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" - def __init__(self, bot: Bot): self.bot = bot self.peps: Dict[int, str] = {} @@ -43,7 +42,7 @@ class PythonEnhancementProposals(Cog): self.last_refreshed_peps = datetime.now() async with self.bot.http_session.get( - self.PEPS_LISTING_API_URL, + PEPS_LISTING_API_URL, headers=GITHUB_API_HEADERS ) as resp: if resp.status != 200: @@ -100,7 +99,7 @@ class PythonEnhancementProposals(Cog): # Assemble the embed pep_embed = Embed( title=f"**PEP {pep_nr} - {pep_header['Title']}**", - description=f"[Link]({self.BASE_PEP_URL}{pep_nr:04})", + description=f"[Link]({BASE_PEP_URL}{pep_nr:04})", ) pep_embed.set_thumbnail(url=ICON_URL) -- cgit v1.2.3 From 361a21205f76a80b54f5816dd96eddda6c55fadb Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 23 Dec 2020 20:12:03 +0200 Subject: Move PEP cog to info extensions category --- bot/exts/info/pep.py | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++ bot/exts/utils/pep.py | 164 -------------------------------------------------- 2 files changed, 164 insertions(+), 164 deletions(-) create mode 100644 bot/exts/info/pep.py delete mode 100644 bot/exts/utils/pep.py diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py new file mode 100644 index 000000000..8ac96bbdb --- /dev/null +++ b/bot/exts/info/pep.py @@ -0,0 +1,164 @@ +import logging +from datetime import datetime, timedelta +from email.parser import HeaderParser +from io import StringIO +from typing import Dict, Optional, Tuple + +from discord import Colour, Embed +from discord.ext.commands import Cog, Context, command + +from bot.bot import Bot +from bot.constants import Keys +from bot.utils.cache import AsyncCache + +log = logging.getLogger(__name__) + +ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" +BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" +PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" + +pep_cache = AsyncCache() + +GITHUB_API_HEADERS = {} +if Keys.github: + GITHUB_API_HEADERS["Authorization"] = f"token {Keys.github}" + + +class PythonEnhancementProposals(Cog): + """Cog for displaying information about PEPs.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.peps: Dict[int, str] = {} + # To avoid situations where we don't have last datetime, set this to now. + self.last_refreshed_peps: datetime = datetime.now() + self.bot.loop.create_task(self.refresh_peps_urls()) + + async def refresh_peps_urls(self) -> None: + """Refresh PEP URLs listing in every 3 hours.""" + # Wait until HTTP client is available + await self.bot.wait_until_ready() + log.trace("Started refreshing PEP URLs.") + self.last_refreshed_peps = datetime.now() + + async with self.bot.http_session.get( + PEPS_LISTING_API_URL, + headers=GITHUB_API_HEADERS + ) as resp: + if resp.status != 200: + log.warning(f"Fetching PEP URLs from GitHub API failed with code {resp.status}") + return + + listing = await resp.json() + + log.trace("Got PEP URLs listing from GitHub API") + + for file in listing: + name = file["name"] + if name.startswith("pep-") and name.endswith((".rst", ".txt")): + pep_number = name.replace("pep-", "").split(".")[0] + self.peps[int(pep_number)] = file["download_url"] + + log.info("Successfully refreshed PEP URLs listing.") + + @staticmethod + def get_pep_zero_embed() -> Embed: + """Get information embed about PEP 0.""" + pep_embed = Embed( + title="**PEP 0 - Index of Python Enhancement Proposals (PEPs)**", + url="https://www.python.org/dev/peps/" + ) + pep_embed.set_thumbnail(url=ICON_URL) + pep_embed.add_field(name="Status", value="Active") + pep_embed.add_field(name="Created", value="13-Jul-2000") + pep_embed.add_field(name="Type", value="Informational") + + return pep_embed + + async def validate_pep_number(self, pep_nr: int) -> Optional[Embed]: + """Validate is PEP number valid. When it isn't, return error embed, otherwise None.""" + if ( + pep_nr not in self.peps + and (self.last_refreshed_peps + timedelta(minutes=30)) <= datetime.now() + and len(str(pep_nr)) < 5 + ): + await self.refresh_peps_urls() + + if pep_nr not in self.peps: + log.trace(f"PEP {pep_nr} was not found") + return Embed( + title="PEP not found", + description=f"PEP {pep_nr} does not exist.", + colour=Colour.red() + ) + + return None + + def generate_pep_embed(self, pep_header: Dict, pep_nr: int) -> Embed: + """Generate PEP embed based on PEP headers data.""" + # Assemble the embed + pep_embed = Embed( + title=f"**PEP {pep_nr} - {pep_header['Title']}**", + description=f"[Link]({BASE_PEP_URL}{pep_nr:04})", + ) + + pep_embed.set_thumbnail(url=ICON_URL) + + # Add the interesting information + fields_to_check = ("Status", "Python-Version", "Created", "Type") + for field in fields_to_check: + # Check for a PEP metadata field that is present but has an empty value + # embed field values can't contain an empty string + if pep_header.get(field, ""): + pep_embed.add_field(name=field, value=pep_header[field]) + + return pep_embed + + @pep_cache(arg_offset=1) + async def get_pep_embed(self, pep_nr: int) -> Tuple[Embed, bool]: + """Fetch, generate and return PEP embed. Second item of return tuple show does getting success.""" + response = await self.bot.http_session.get(self.peps[pep_nr]) + + if response.status == 200: + log.trace(f"PEP {pep_nr} found") + pep_content = await response.text() + + # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179 + pep_header = HeaderParser().parse(StringIO(pep_content)) + return self.generate_pep_embed(pep_header, pep_nr), True + else: + log.trace( + f"The user requested PEP {pep_nr}, but the response had an unexpected status code: {response.status}." + ) + return Embed( + title="Unexpected error", + description="Unexpected HTTP error during PEP search. Please let us know.", + colour=Colour.red() + ), False + + @command(name='pep', aliases=('get_pep', 'p')) + async def pep_command(self, ctx: Context, pep_number: int) -> None: + """Fetches information about a PEP and sends it to the channel.""" + # Trigger typing in chat to show users that bot is responding + await ctx.trigger_typing() + + # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs. + if pep_number == 0: + pep_embed = self.get_pep_zero_embed() + success = True + else: + success = False + if not (pep_embed := await self.validate_pep_number(pep_number)): + pep_embed, success = await self.get_pep_embed(pep_number) + + await ctx.send(embed=pep_embed) + if success: + log.trace(f"PEP {pep_number} getting and sending finished successfully. Increasing stat.") + self.bot.stats.incr(f"pep_fetches.{pep_number}") + else: + log.trace(f"Getting PEP {pep_number} failed. Error embed sent.") + + +def setup(bot: Bot) -> None: + """Load the PEP cog.""" + bot.add_cog(PythonEnhancementProposals(bot)) diff --git a/bot/exts/utils/pep.py b/bot/exts/utils/pep.py deleted file mode 100644 index 8ac96bbdb..000000000 --- a/bot/exts/utils/pep.py +++ /dev/null @@ -1,164 +0,0 @@ -import logging -from datetime import datetime, timedelta -from email.parser import HeaderParser -from io import StringIO -from typing import Dict, Optional, Tuple - -from discord import Colour, Embed -from discord.ext.commands import Cog, Context, command - -from bot.bot import Bot -from bot.constants import Keys -from bot.utils.cache import AsyncCache - -log = logging.getLogger(__name__) - -ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" -BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" -PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" - -pep_cache = AsyncCache() - -GITHUB_API_HEADERS = {} -if Keys.github: - GITHUB_API_HEADERS["Authorization"] = f"token {Keys.github}" - - -class PythonEnhancementProposals(Cog): - """Cog for displaying information about PEPs.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.peps: Dict[int, str] = {} - # To avoid situations where we don't have last datetime, set this to now. - self.last_refreshed_peps: datetime = datetime.now() - self.bot.loop.create_task(self.refresh_peps_urls()) - - async def refresh_peps_urls(self) -> None: - """Refresh PEP URLs listing in every 3 hours.""" - # Wait until HTTP client is available - await self.bot.wait_until_ready() - log.trace("Started refreshing PEP URLs.") - self.last_refreshed_peps = datetime.now() - - async with self.bot.http_session.get( - PEPS_LISTING_API_URL, - headers=GITHUB_API_HEADERS - ) as resp: - if resp.status != 200: - log.warning(f"Fetching PEP URLs from GitHub API failed with code {resp.status}") - return - - listing = await resp.json() - - log.trace("Got PEP URLs listing from GitHub API") - - for file in listing: - name = file["name"] - if name.startswith("pep-") and name.endswith((".rst", ".txt")): - pep_number = name.replace("pep-", "").split(".")[0] - self.peps[int(pep_number)] = file["download_url"] - - log.info("Successfully refreshed PEP URLs listing.") - - @staticmethod - def get_pep_zero_embed() -> Embed: - """Get information embed about PEP 0.""" - pep_embed = Embed( - title="**PEP 0 - Index of Python Enhancement Proposals (PEPs)**", - url="https://www.python.org/dev/peps/" - ) - pep_embed.set_thumbnail(url=ICON_URL) - pep_embed.add_field(name="Status", value="Active") - pep_embed.add_field(name="Created", value="13-Jul-2000") - pep_embed.add_field(name="Type", value="Informational") - - return pep_embed - - async def validate_pep_number(self, pep_nr: int) -> Optional[Embed]: - """Validate is PEP number valid. When it isn't, return error embed, otherwise None.""" - if ( - pep_nr not in self.peps - and (self.last_refreshed_peps + timedelta(minutes=30)) <= datetime.now() - and len(str(pep_nr)) < 5 - ): - await self.refresh_peps_urls() - - if pep_nr not in self.peps: - log.trace(f"PEP {pep_nr} was not found") - return Embed( - title="PEP not found", - description=f"PEP {pep_nr} does not exist.", - colour=Colour.red() - ) - - return None - - def generate_pep_embed(self, pep_header: Dict, pep_nr: int) -> Embed: - """Generate PEP embed based on PEP headers data.""" - # Assemble the embed - pep_embed = Embed( - title=f"**PEP {pep_nr} - {pep_header['Title']}**", - description=f"[Link]({BASE_PEP_URL}{pep_nr:04})", - ) - - pep_embed.set_thumbnail(url=ICON_URL) - - # Add the interesting information - fields_to_check = ("Status", "Python-Version", "Created", "Type") - for field in fields_to_check: - # Check for a PEP metadata field that is present but has an empty value - # embed field values can't contain an empty string - if pep_header.get(field, ""): - pep_embed.add_field(name=field, value=pep_header[field]) - - return pep_embed - - @pep_cache(arg_offset=1) - async def get_pep_embed(self, pep_nr: int) -> Tuple[Embed, bool]: - """Fetch, generate and return PEP embed. Second item of return tuple show does getting success.""" - response = await self.bot.http_session.get(self.peps[pep_nr]) - - if response.status == 200: - log.trace(f"PEP {pep_nr} found") - pep_content = await response.text() - - # Taken from https://github.com/python/peps/blob/master/pep0/pep.py#L179 - pep_header = HeaderParser().parse(StringIO(pep_content)) - return self.generate_pep_embed(pep_header, pep_nr), True - else: - log.trace( - f"The user requested PEP {pep_nr}, but the response had an unexpected status code: {response.status}." - ) - return Embed( - title="Unexpected error", - description="Unexpected HTTP error during PEP search. Please let us know.", - colour=Colour.red() - ), False - - @command(name='pep', aliases=('get_pep', 'p')) - async def pep_command(self, ctx: Context, pep_number: int) -> None: - """Fetches information about a PEP and sends it to the channel.""" - # Trigger typing in chat to show users that bot is responding - await ctx.trigger_typing() - - # Handle PEP 0 directly because it's not in .rst or .txt so it can't be accessed like other PEPs. - if pep_number == 0: - pep_embed = self.get_pep_zero_embed() - success = True - else: - success = False - if not (pep_embed := await self.validate_pep_number(pep_number)): - pep_embed, success = await self.get_pep_embed(pep_number) - - await ctx.send(embed=pep_embed) - if success: - log.trace(f"PEP {pep_number} getting and sending finished successfully. Increasing stat.") - self.bot.stats.incr(f"pep_fetches.{pep_number}") - else: - log.trace(f"Getting PEP {pep_number} failed. Error embed sent.") - - -def setup(bot: Bot) -> None: - """Load the PEP cog.""" - bot.add_cog(PythonEnhancementProposals(bot)) -- cgit v1.2.3 From f397102efc3c1551f37d1ac9cb45d07043487a37 Mon Sep 17 00:00:00 2001 From: Steele Date: Wed, 23 Dec 2020 18:54:01 -0500 Subject: `ALTERNATE_VERIFIED_MESSAGE`: "You're" -> "You are". --- bot/exts/moderation/verification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index dbd3c42a6..6a4319705 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -55,7 +55,7 @@ If you'd like to unsubscribe from the announcement notifications, simply send `! """ ALTERNATE_VERIFIED_MESSAGE = f""" -You're now verified! +You are now verified! You can find a copy of our rules for reference at . -- cgit v1.2.3 From 68cbca003c508dd7287120e73a558e160f09c276 Mon Sep 17 00:00:00 2001 From: Steele Farnsworth <32915757+swfarnsworth@users.noreply.github.com> Date: Thu, 24 Dec 2020 10:35:19 -0500 Subject: `if user.pending` -> `if not user.pending` This was a logic error. This functionality is unfortunately difficult to test outside of production. --- bot/exts/moderation/verification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index 6a4319705..ce91dcb15 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -838,7 +838,7 @@ class Verification(Cog): """Command for moderators to verify any user.""" log.trace(f'verify command called by {ctx.author} for {user.id}.') - if user.pending: + if not user.pending: log.trace(f'{user.id} is already verified, aborting.') await ctx.send(f'{constants.Emojis.cross_mark} {user.mention} is already verified.') return -- cgit v1.2.3 From dd2497338721dff3d34b7127883c2c6d65cd08c5 Mon Sep 17 00:00:00 2001 From: Steele Date: Fri, 25 Dec 2020 14:10:43 -0500 Subject: `!user` command says if user is "Verified" Previously, `!user` said if the user is "Pending", whereas "Verified" is the boolean opposite. --- bot/exts/info/information.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 2057876e4..b2138b03f 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -229,9 +229,9 @@ class Information(Cog): if on_server: joined = time_since(user.joined_at, max_units=3) roles = ", ".join(role.mention for role in user.roles[1:]) - membership = {"Joined": joined, "Pending": user.pending, "Roles": roles or None} + membership = {"Joined": joined, "Verified": not user.pending, "Roles": roles or None} if not is_mod_channel(ctx.channel): - membership.pop("Pending") + membership.pop("Verified") membership = textwrap.dedent("\n".join([f"{key}: {value}" for key, value in membership.items()])) else: -- cgit v1.2.3 From fc8a1246b281fd0d495955e0b84c6fc75a59ba4d Mon Sep 17 00:00:00 2001 From: Steele Date: Wed, 30 Dec 2020 16:39:49 -0500 Subject: "Pending: False" to "Verified: True" to agree with new semantics. --- tests/bot/exts/info/test_information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 043cce8de..d077be960 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -355,7 +355,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): self.assertEqual( textwrap.dedent(f""" Joined: {"1 year ago"} - Pending: {"False"} + Verified: {"True"} Roles: &Moderators """).strip(), embed.fields[1].value -- cgit v1.2.3