From aae80011f5cb7e1ec5b9d6fd648ba255ad30e0df Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 30 Oct 2020 05:31:09 +0200 Subject: Added defcon status notifier --- bot/exts/moderation/defcon.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index caa6fb917..4b25c36df 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -4,8 +4,10 @@ import logging from collections import namedtuple from datetime import datetime, timedelta from enum import Enum +from gettext import ngettext from discord import Colour, Embed, Member +from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot @@ -83,6 +85,7 @@ class Defcon(Cog): self.days = timedelta(days=0) log.info("DEFCON disabled") + self.update_notifier() await self.update_channel_topic() @Cog.listener() @@ -153,6 +156,10 @@ class Defcon(Cog): } } ) + + self.days = timedelta(days=days) + self.update_notifier() + except Exception as err: log.exception("Unable to update DEFCON settings.") error = err @@ -199,7 +206,6 @@ class Defcon(Cog): @has_any_role(*MODERATION_ROLES) async def days_command(self, ctx: Context, days: int) -> None: """Set how old an account must be to join the server, in days, with DEFCON mode enabled.""" - self.days = timedelta(days=days) self.enabled = True await self._defcon_action(ctx, days=days, action=Action.UPDATED) await self.update_channel_topic() @@ -252,6 +258,21 @@ class Defcon(Cog): await self.mod_log.send_log_message(info.icon, info.color, status_msg, log_msg) + def update_notifier(self) -> None: + """Start or stop the notifier according to the DEFCON status.""" + if self.days.days != 0 and not self.defcon_notifier.is_running(): + log.info("DEFCON notifier started.") + self.defcon_notifier.start() + + elif self.days.days == 0 and self.defcon_notifier.is_running(): + log.info("DEFCON notifier stopped.") + self.defcon_notifier.cancel() + + @tasks.loop(hours=1) + async def defcon_notifier(self) -> None: + """Routinely notify moderators that DEFCON is active.""" + await self.channel.send(f"Defcon is on and is set to {self.days.days} day{ngettext('', 's', self.days.days)}.") + def setup(bot: Bot) -> None: """Load the Defcon cog.""" -- cgit v1.2.3 From f23c2e78fb9ac6e6c2f7faeaeaf652c89ad8c263 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 26 Jan 2021 18:06:32 +0200 Subject: Make the cog update even if write to DB fails The defcon cog should be functional even if there is some issue with writing to the DB for some reason. The functionality should have retention across restarts, but it shouldn't be its failing point. If necessary, it should be able to work with no DB at all --- bot/exts/moderation/defcon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 4b25c36df..00b108feb 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -157,13 +157,13 @@ class Defcon(Cog): } ) - self.days = timedelta(days=days) - self.update_notifier() - except Exception as err: log.exception("Unable to update DEFCON settings.") error = err finally: + self.days = timedelta(days=days) + self.update_notifier() + await ctx.send(self.build_defcon_msg(action, error)) await self.send_defcon_log(action, ctx.author, error) -- cgit v1.2.3 From f2c8e29f79c19cbef0d0477b668d30aca5efb099 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 26 Jan 2021 19:36:35 +0200 Subject: Moved self.enabled update to _defcon_action --- bot/exts/moderation/defcon.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 00b108feb..f34f8fa28 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -44,13 +44,11 @@ class Action(Enum): class Defcon(Cog): """Time-sensitive server defense mechanisms.""" - days = None # type: timedelta - enabled = False # type: bool - def __init__(self, bot: Bot): self.bot = bot self.channel = None self.days = timedelta(days=0) + self.enabled = False self.bot.loop.create_task(self.sync_settings()) @@ -142,6 +140,9 @@ class Defcon(Cog): except Exception: pass + self.days = timedelta(days=days) + self.enabled = action != Action.DISABLED + error = None try: await self.bot.api_client.put( @@ -150,8 +151,8 @@ class Defcon(Cog): 'name': 'defcon', 'data': { # TODO: retrieve old days count - 'days': days, - 'enabled': action is not Action.DISABLED, + 'days': self.days.days, + 'enabled': self.enabled, 'enable_date': datetime.now().isoformat() } } @@ -161,7 +162,6 @@ class Defcon(Cog): log.exception("Unable to update DEFCON settings.") error = err finally: - self.days = timedelta(days=days) self.update_notifier() await ctx.send(self.build_defcon_msg(action, error)) @@ -178,7 +178,6 @@ class Defcon(Cog): Currently, this just adds an account age requirement. Use !defcon days to set how old an account must be, in days. """ - self.enabled = True await self._defcon_action(ctx, days=0, action=Action.ENABLED) await self.update_channel_topic() @@ -186,7 +185,6 @@ class Defcon(Cog): @has_any_role(*MODERATION_ROLES) async def disable_command(self, ctx: Context) -> None: """Disable DEFCON mode. Useful in a pinch, but be sure you know what you're doing!""" - self.enabled = False await self._defcon_action(ctx, days=0, action=Action.DISABLED) await self.update_channel_topic() @@ -206,7 +204,6 @@ class Defcon(Cog): @has_any_role(*MODERATION_ROLES) async def days_command(self, ctx: Context, days: int) -> None: """Set how old an account must be to join the server, in days, with DEFCON mode enabled.""" - self.enabled = True await self._defcon_action(ctx, days=days, action=Action.UPDATED) await self.update_channel_topic() -- cgit v1.2.3 From 76574adda0e4a033b93b976278904d796ef055aa Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 26 Jan 2021 19:41:20 +0200 Subject: Moved channel topic change to _defcon_action --- bot/exts/moderation/defcon.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index f34f8fa28..1e88a8d9c 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -166,6 +166,7 @@ class Defcon(Cog): await ctx.send(self.build_defcon_msg(action, error)) await self.send_defcon_log(action, ctx.author, error) + await self.update_channel_topic() self.bot.stats.gauge("defcon.threshold", days) @@ -179,14 +180,12 @@ class Defcon(Cog): in days. """ await self._defcon_action(ctx, days=0, action=Action.ENABLED) - await self.update_channel_topic() @defcon_group.command(name='disable', aliases=('off', 'd'), root_aliases=("defoff",)) @has_any_role(*MODERATION_ROLES) async def disable_command(self, ctx: Context) -> None: """Disable DEFCON mode. Useful in a pinch, but be sure you know what you're doing!""" await self._defcon_action(ctx, days=0, action=Action.DISABLED) - await self.update_channel_topic() @defcon_group.command(name='status', aliases=('s',)) @has_any_role(*MODERATION_ROLES) @@ -205,7 +204,6 @@ class Defcon(Cog): async def days_command(self, ctx: Context, days: int) -> None: """Set how old an account must be to join the server, in days, with DEFCON mode enabled.""" await self._defcon_action(ctx, days=days, action=Action.UPDATED) - await self.update_channel_topic() async def update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" -- cgit v1.2.3 From e3949433fc87cd58f1e0645756bd0d8de60798ee Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 26 Jan 2021 20:03:45 +0200 Subject: Added cog unloader to cancel notifier --- bot/exts/moderation/defcon.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 1e88a8d9c..a180d7aae 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -59,7 +59,10 @@ class Defcon(Cog): async def sync_settings(self) -> None: """On cog load, try to synchronize DEFCON settings to the API.""" + log.trace("Waiting for the guild to become available before syncing.") await self.bot.wait_until_guild_available() + + log.trace("Syncing settings.") self.channel = await self.bot.fetch_channel(Channels.defcon) try: @@ -268,6 +271,11 @@ class Defcon(Cog): """Routinely notify moderators that DEFCON is active.""" await self.channel.send(f"Defcon is on and is set to {self.days.days} day{ngettext('', 's', self.days.days)}.") + def cog_unload(self) -> None: + """Cancel the notifer task when the cog unloads.""" + log.trace("Cog unload: canceling defcon notifier task.") + self.defcon_notifier.cancel() + def setup(bot: Bot) -> None: """Load the Defcon cog.""" -- cgit v1.2.3 From aeaebbd9a49afa5e53070afc5498ad5a25cad6fe Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 26 Jan 2021 21:51:02 +0200 Subject: Defon doesn't reset the number of days --- bot/exts/moderation/defcon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index a180d7aae..e0baab099 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -182,7 +182,7 @@ class Defcon(Cog): Currently, this just adds an account age requirement. Use !defcon days to set how old an account must be, in days. """ - await self._defcon_action(ctx, days=0, action=Action.ENABLED) + await self._defcon_action(ctx, days=self.days, action=Action.ENABLED) @defcon_group.command(name='disable', aliases=('off', 'd'), root_aliases=("defoff",)) @has_any_role(*MODERATION_ROLES) -- cgit v1.2.3 From 085f8ac801316c6d8ad91a3b63b3e755c3a6aea3 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 31 Jan 2021 15:32:30 +0000 Subject: return true when tag is on cd --- bot/exts/info/tags.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 00b4d1a78..e0556ee30 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -189,7 +189,7 @@ class Tags(Cog): If a tag is not specified, display a paginated embed of all tags. Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display - nothing and return False. + nothing and return True. """ def _command_on_cooldown(tag_name: str) -> bool: """ @@ -217,7 +217,7 @@ class Tags(Cog): f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. " f"Cooldown ends in {time_left:.1f} seconds." ) - return False + return True if tag_name is not None: temp_founds = self._get_tag(tag_name) @@ -285,7 +285,8 @@ class Tags(Cog): """ Get a specified tag, or a list of all tags if no tag is specified. - Returns False if a tag is on cooldown, or if no matches are found. + Returns True if something can be send, or the tag is on cooldown + Returns False if no matches are found. """ return await self.display_tag(ctx, tag_name) -- cgit v1.2.3 From cc23ad61e3a9d33ff835c04c2c8d0ddcb45de736 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 31 Jan 2021 15:35:56 +0000 Subject: Grammar --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index e0556ee30..bb91a8563 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -285,7 +285,7 @@ class Tags(Cog): """ Get a specified tag, or a list of all tags if no tag is specified. - Returns True if something can be send, or the tag is on cooldown + Returns True if something can be sent, or if the tag is on cooldown. Returns False if no matches are found. """ return await self.display_tag(ctx, tag_name) -- cgit v1.2.3 From 01ad92b2d3c4a3679f86ac8889736fa873e00ae4 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Sun, 31 Jan 2021 17:35:30 +0100 Subject: Re created file from last point --- bot/resources/tags/empty-json.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 bot/resources/tags/empty-json.md diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md new file mode 100644 index 000000000..a54d3d71a --- /dev/null +++ b/bot/resources/tags/empty-json.md @@ -0,0 +1,27 @@ +When creating a new JSON file you might run into the following error. + +`JSONDecodeError: Expecting value: line 1 column 1 (char 0)` + +In short this error means your JSON is invalid in it's current state. +A JSON may never be completely empty and must always at least have one of the following items. + +``` +object +array +string +number +"true" +"false" +"null" +``` + +To resolve this issue, you create one of the above values in your JSON. It is very common to use `{}` to make an object. Adding the following to your JSON should resolve this issue. + +```json +{ + + +} +``` + +Make sure to put all your data between the `{}`, just like you would when making a dictionary. \ No newline at end of file -- cgit v1.2.3 From 5a6f77fde58f024ea151adfdc6a5745eeb0046cd Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Sun, 31 Jan 2021 17:41:06 +0100 Subject: name fix, added suggestions from previous PR --- bot/resources/tags/empty-json.md | 27 --------------------------- bot/resources/tags/empty_json.md | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 27 deletions(-) delete mode 100644 bot/resources/tags/empty-json.md create mode 100644 bot/resources/tags/empty_json.md diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md deleted file mode 100644 index a54d3d71a..000000000 --- a/bot/resources/tags/empty-json.md +++ /dev/null @@ -1,27 +0,0 @@ -When creating a new JSON file you might run into the following error. - -`JSONDecodeError: Expecting value: line 1 column 1 (char 0)` - -In short this error means your JSON is invalid in it's current state. -A JSON may never be completely empty and must always at least have one of the following items. - -``` -object -array -string -number -"true" -"false" -"null" -``` - -To resolve this issue, you create one of the above values in your JSON. It is very common to use `{}` to make an object. Adding the following to your JSON should resolve this issue. - -```json -{ - - -} -``` - -Make sure to put all your data between the `{}`, just like you would when making a dictionary. \ No newline at end of file diff --git a/bot/resources/tags/empty_json.md b/bot/resources/tags/empty_json.md new file mode 100644 index 000000000..36511abb6 --- /dev/null +++ b/bot/resources/tags/empty_json.md @@ -0,0 +1,24 @@ +When creating a new JSON file you might run into the following error. + +`JSONDecodeError: Expecting value: line 1 column 1 (char 0)` + +In short, this means that your JSON is invalid in its current state. This could very well happen because the file is just new and empty. +A JSON may never be completely empty. It is recommended to have at least one of the following in your json: + +``` +object +array +``` + +To resolve this issue, you create one of the above values in your JSON. It is very common to use `{}` to make an object, which is similar to a dictionary in python. +When this is added to your JSON, it will look like this: + +```json +{ + + +} +``` + +The error is resolved now. +Make sure to put all your data between the `{}`, just like you would when making a dictionary. \ No newline at end of file -- cgit v1.2.3 From 4704344807bf56a544ddeb8cdc592bcb69675cf6 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Sun, 31 Jan 2021 17:48:09 +0100 Subject: Fixed EOF --- bot/resources/tags/empty_json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/empty_json.md b/bot/resources/tags/empty_json.md index 36511abb6..8fb7c4e23 100644 --- a/bot/resources/tags/empty_json.md +++ b/bot/resources/tags/empty_json.md @@ -21,4 +21,4 @@ When this is added to your JSON, it will look like this: ``` The error is resolved now. -Make sure to put all your data between the `{}`, just like you would when making a dictionary. \ No newline at end of file +Make sure to put all your data between the `{}`, just like you would when making a dictionary. -- cgit v1.2.3 From 9931ebc20ff3dcda11cd2bca338c3a798e6f6b17 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Mon, 1 Feb 2021 18:29:48 +0100 Subject: Removed extra whitespace line in last example --- bot/resources/tags/empty_json.md | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/resources/tags/empty_json.md b/bot/resources/tags/empty_json.md index 8fb7c4e23..d5e0f843f 100644 --- a/bot/resources/tags/empty_json.md +++ b/bot/resources/tags/empty_json.md @@ -16,7 +16,6 @@ When this is added to your JSON, it will look like this: ```json { - } ``` -- cgit v1.2.3 From dc8a5d4084e124f9a7d6e0d31658b6eb0637bccc Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Mon, 1 Feb 2021 18:41:09 +0100 Subject: Changed some wording --- bot/resources/tags/empty_json.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/resources/tags/empty_json.md b/bot/resources/tags/empty_json.md index d5e0f843f..45df3fd54 100644 --- a/bot/resources/tags/empty_json.md +++ b/bot/resources/tags/empty_json.md @@ -2,16 +2,16 @@ When creating a new JSON file you might run into the following error. `JSONDecodeError: Expecting value: line 1 column 1 (char 0)` -In short, this means that your JSON is invalid in its current state. This could very well happen because the file is just new and empty. -A JSON may never be completely empty. It is recommended to have at least one of the following in your json: +In short, this means that your JSON is invalid in its current state. This could very well happen because the file is just new and completely empty. +Whilst the JSON data may be empty, the .json file must not. It is recommended to have at least one of the following data types in your .json file: ``` object array ``` -To resolve this issue, you create one of the above values in your JSON. It is very common to use `{}` to make an object, which is similar to a dictionary in python. -When this is added to your JSON, it will look like this: +To resolve this issue, you create one of the above data types in your .json file. It is very common to use `{}` to make an object, which works similar to a dictionary in python. +When this is added to your .json file, it will look like this: ```json { -- cgit v1.2.3 From 5040129e8b32ace05d8b391c1753a96769555bab Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Mon, 1 Feb 2021 18:42:46 +0100 Subject: added some more clarification --- bot/resources/tags/empty_json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/empty_json.md b/bot/resources/tags/empty_json.md index 45df3fd54..eaeafeb18 100644 --- a/bot/resources/tags/empty_json.md +++ b/bot/resources/tags/empty_json.md @@ -3,7 +3,7 @@ When creating a new JSON file you might run into the following error. `JSONDecodeError: Expecting value: line 1 column 1 (char 0)` In short, this means that your JSON is invalid in its current state. This could very well happen because the file is just new and completely empty. -Whilst the JSON data may be empty, the .json file must not. It is recommended to have at least one of the following data types in your .json file: +Whilst the JSON data, the data you wish to store, may be empty, the .json file must not. It is recommended to have at least one of the following data types in your .json file: ``` object -- cgit v1.2.3 From 7b4833ed11f96d1e8bc26ec3997ee42956dca230 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Mon, 1 Feb 2021 18:45:15 +0100 Subject: suggestion for: more friendly/less personal suggestion. --- bot/resources/tags/empty_json.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/empty_json.md b/bot/resources/tags/empty_json.md index eaeafeb18..9e5c5fd4f 100644 --- a/bot/resources/tags/empty_json.md +++ b/bot/resources/tags/empty_json.md @@ -3,14 +3,14 @@ When creating a new JSON file you might run into the following error. `JSONDecodeError: Expecting value: line 1 column 1 (char 0)` In short, this means that your JSON is invalid in its current state. This could very well happen because the file is just new and completely empty. -Whilst the JSON data, the data you wish to store, may be empty, the .json file must not. It is recommended to have at least one of the following data types in your .json file: +Whilst the JSON data, the data you wish to store, may be empty, the .json file must not. You most likely want to use one of the following data types in your .json file: ``` object array ``` -To resolve this issue, you create one of the above data types in your .json file. It is very common to use `{}` to make an object, which works similar to a dictionary in python. +To resolve this issue, create one of the above data types in your .json file. It is very common to use `{}` to make an object, which works similar to a dictionary in python. When this is added to your .json file, it will look like this: ```json -- cgit v1.2.3 From 0f46bf58bea83da9434b53ddfda3ce8331829588 Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 4 Feb 2021 16:07:37 -0800 Subject: Added dynamic available help channels message --- bot/exts/help_channels/_cog.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 0995c8a79..c4ec820b5 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -11,6 +11,7 @@ from discord.ext import commands from bot import constants from bot.bot import Bot +from bot.constants import Channels, Categories from bot.exts.help_channels import _caches, _channel, _cooldown, _message, _name, _stats from bot.utils import channel as channel_utils, lock, scheduling @@ -20,6 +21,9 @@ NAMESPACE = "help" HELP_CHANNEL_TOPIC = """ This is a Python help channel. You can claim your own help channel in the Python Help: Available category. """ +AVAILABLE_HELP_CHANNELS = """ +Currently available help channel(s): {available} +""" class HelpChannels(commands.Cog): @@ -72,6 +76,11 @@ class HelpChannels(commands.Cog): self.last_notification: t.Optional[datetime] = None + # Acquiring and modifying the channel to dynamically update the available help channels message. + self.how_to_get_help: discord.TextChannel = None + self.available_help_channels: t.Set[int] = set() + self.dynamic_message: discord.Message = None + # Asyncio stuff self.queue_tasks: t.List[asyncio.Task] = [] self.init_task = self.bot.loop.create_task(self.init_cog()) @@ -114,6 +123,9 @@ class HelpChannels(commands.Cog): await _caches.unanswered.set(message.channel.id, True) + self.available_help_channels.remove(message.channel.id) + await self.update_available_help_channels() + # Not awaited because it may indefinitely hold the lock while waiting for a channel. scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}") @@ -275,6 +287,15 @@ class HelpChannels(commands.Cog): # This may confuse users. So would potentially long delays for the cog to become ready. self.close_command.enabled = True + # Getting channels that need to be included in the dynamic message. + task = asyncio.create_task(self.update_available_help_channels()) + self.queue_tasks.append(task) + + await task + + log.trace(f"Dynamic available help message updated.") + self.queue_tasks.remove(task) + await self.init_available() _stats.report_counts() @@ -332,6 +353,10 @@ class HelpChannels(commands.Cog): category_id=constants.Categories.help_available, ) + # Adding the help channel to the dynamic message, and editing/sending that message. + self.available_help_channels.add(channel.id) + await self.update_available_help_channels() + _stats.report_counts() async def move_to_dormant(self, channel: discord.TextChannel) -> None: @@ -461,3 +486,26 @@ class HelpChannels(commands.Cog): self.queue_tasks.remove(task) return channel + + async def update_available_help_channels(self) -> None: + """Updates the dynamic message within #how-to-get-help for available help channels.""" + if not self.available_help_channels: + available_channels_category = await channel_utils.try_get_channel(Categories.help_available) + self.available_help_channels = set( + c.id for c in available_channels_category.channels if 'help-' in c.name + ) + + if self.how_to_get_help is None: + self.how_to_get_help = await channel_utils.try_get_channel(Channels.how_to_get_help) + + if self.dynamic_message is None: + self.dynamic_message = await self.how_to_get_help.history(limit=1).next() + + available_channels = AVAILABLE_HELP_CHANNELS.format( + available=', '.join(f"<#{c}>" for c in self.available_help_channels) + ) + + try: + await self.dynamic_message.edit(content=available_channels) + except discord.Forbidden: + self.dynamic_message = await self.how_to_get_help.send(available_channels) -- cgit v1.2.3 From 5f17d4d19d0952c91ead096a52b206eea86851fe Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 4 Feb 2021 16:18:20 -0800 Subject: Fixed up linting errors. --- bot/exts/help_channels/_cog.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index c4ec820b5..943b63a42 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -11,7 +11,7 @@ from discord.ext import commands from bot import constants from bot.bot import Bot -from bot.constants import Channels, Categories +from bot.constants import Categories, Channels from bot.exts.help_channels import _caches, _channel, _cooldown, _message, _name, _stats from bot.utils import channel as channel_utils, lock, scheduling @@ -293,7 +293,7 @@ class HelpChannels(commands.Cog): await task - log.trace(f"Dynamic available help message updated.") + log.trace("Dynamic available help message updated.") self.queue_tasks.remove(task) await self.init_available() @@ -499,7 +499,8 @@ class HelpChannels(commands.Cog): self.how_to_get_help = await channel_utils.try_get_channel(Channels.how_to_get_help) if self.dynamic_message is None: - self.dynamic_message = await self.how_to_get_help.history(limit=1).next() + last_message = await self.how_to_get_help.history(limit=1) + self.dynamic_message = next(last_message) available_channels = AVAILABLE_HELP_CHANNELS.format( available=', '.join(f"<#{c}>" for c in self.available_help_channels) -- cgit v1.2.3 From 219ac90d494793a99d77ef5a4e912151e936b1d8 Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 4 Feb 2021 17:12:25 -0800 Subject: Fixed logic in case dynamic message doesn't exist. --- bot/exts/help_channels/_cog.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 943b63a42..730635f08 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -495,18 +495,16 @@ class HelpChannels(commands.Cog): c.id for c in available_channels_category.channels if 'help-' in c.name ) - if self.how_to_get_help is None: - self.how_to_get_help = await channel_utils.try_get_channel(Channels.how_to_get_help) - - if self.dynamic_message is None: - last_message = await self.how_to_get_help.history(limit=1) - self.dynamic_message = next(last_message) - available_channels = AVAILABLE_HELP_CHANNELS.format( available=', '.join(f"<#{c}>" for c in self.available_help_channels) ) - try: - await self.dynamic_message.edit(content=available_channels) - except discord.Forbidden: - self.dynamic_message = await self.how_to_get_help.send(available_channels) + if self.how_to_get_help is None: + self.how_to_get_help = await channel_utils.try_get_channel(Channels.how_to_get_help) + + if self.dynamic_message is None: + try: + self.dynamic_message = await self.how_to_get_help.fetch_message(self.how_to_get_help.last_message_id) + await self.dynamic_message.edit(content=available_channels) + except discord.NotFound: + self.dynamic_message = await self.how_to_get_help.send(available_channels) -- cgit v1.2.3 From 12d8670de79011dc1095293ddc7b256f033fcead Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 4 Feb 2021 18:22:21 -0800 Subject: 'None' is now shown if no channels are available. --- bot/exts/help_channels/_cog.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 730635f08..2f14146ab 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -123,12 +123,13 @@ class HelpChannels(commands.Cog): await _caches.unanswered.set(message.channel.id, True) - self.available_help_channels.remove(message.channel.id) - await self.update_available_help_channels() - # Not awaited because it may indefinitely hold the lock while waiting for a channel. scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}") + # Removing the help channel from the dynamic message, and editing/sending that message. + self.available_help_channels.remove(message.channel.id) + await self.update_available_help_channels() + def create_channel_queue(self) -> asyncio.Queue: """ Return a queue of dormant channels to use for getting the next available channel. @@ -496,15 +497,15 @@ class HelpChannels(commands.Cog): ) available_channels = AVAILABLE_HELP_CHANNELS.format( - available=', '.join(f"<#{c}>" for c in self.available_help_channels) + available=', '.join(f"<#{c}>" for c in self.available_help_channels) or None ) if self.how_to_get_help is None: self.how_to_get_help = await channel_utils.try_get_channel(Channels.how_to_get_help) - if self.dynamic_message is None: - try: + try: + if self.dynamic_message is None: self.dynamic_message = await self.how_to_get_help.fetch_message(self.how_to_get_help.last_message_id) - await self.dynamic_message.edit(content=available_channels) - except discord.NotFound: - self.dynamic_message = await self.how_to_get_help.send(available_channels) + await self.dynamic_message.edit(content=available_channels) + except discord.NotFound: + self.dynamic_message = await self.how_to_get_help.send(available_channels) -- cgit v1.2.3 From d2a10cd1b758625ea165e599c0271d9e43f0ab8a Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 4 Feb 2021 18:35:11 -0800 Subject: Alphabetized config-default.yml. --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 913d5ca09..fc1f3b3a8 100644 --- a/config-default.yml +++ b/config-default.yml @@ -158,8 +158,8 @@ guild: python_general: &PY_GENERAL 267624335836053506 # Python Help: Available - how_to_get_help: 704250143020417084 cooldown: 720603994149486673 + how_to_get_help: 704250143020417084 # Topical discord_py: 343944376055103488 -- cgit v1.2.3 From 3a4d38bc27dc8214af91ee8ab598a5b60897815f Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 4 Feb 2021 18:53:54 -0800 Subject: Removed unnecessary update method call. --- bot/exts/help_channels/_cog.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 2f14146ab..d50197339 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -123,12 +123,11 @@ class HelpChannels(commands.Cog): await _caches.unanswered.set(message.channel.id, True) - # Not awaited because it may indefinitely hold the lock while waiting for a channel. - scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}") - # Removing the help channel from the dynamic message, and editing/sending that message. self.available_help_channels.remove(message.channel.id) - await self.update_available_help_channels() + + # Not awaited because it may indefinitely hold the lock while waiting for a channel. + scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}") def create_channel_queue(self) -> asyncio.Queue: """ -- cgit v1.2.3 From 88b88ba5c42f7db2d253493fa9b3749287d31ffb Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 4 Feb 2021 19:14:02 -0800 Subject: Replaced fetching available category for old one. --- bot/exts/help_channels/_cog.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index d50197339..4520b408d 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -11,7 +11,7 @@ from discord.ext import commands from bot import constants from bot.bot import Bot -from bot.constants import Categories, Channels +from bot.constants import Channels from bot.exts.help_channels import _caches, _channel, _cooldown, _message, _name, _stats from bot.utils import channel as channel_utils, lock, scheduling @@ -490,9 +490,8 @@ class HelpChannels(commands.Cog): async def update_available_help_channels(self) -> None: """Updates the dynamic message within #how-to-get-help for available help channels.""" if not self.available_help_channels: - available_channels_category = await channel_utils.try_get_channel(Categories.help_available) self.available_help_channels = set( - c.id for c in available_channels_category.channels if 'help-' in c.name + c.id for c in self.available_category.channels if 'help-' in c.name ) available_channels = AVAILABLE_HELP_CHANNELS.format( -- cgit v1.2.3 From 2940ba31bb5118f7c9668d14c3d9ffa7611b3890 Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 5 Feb 2021 02:55:50 -0800 Subject: Modified the dynamic to be bold to catch eyes. --- bot/exts/help_channels/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 4520b408d..e9333b9a6 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -22,7 +22,7 @@ HELP_CHANNEL_TOPIC = """ This is a Python help channel. You can claim your own help channel in the Python Help: Available category. """ AVAILABLE_HELP_CHANNELS = """ -Currently available help channel(s): {available} +**Currently available help channel(s):** {available} """ -- cgit v1.2.3 From 555c1a8a4543f051b950c72a4b89805db988ca76 Mon Sep 17 00:00:00 2001 From: Senjan21 <53477086+Senjan21@users.noreply.github.com> Date: Fri, 5 Feb 2021 12:29:27 +0100 Subject: Add jumplink to deleted messages --- bot/exts/moderation/modlog.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index e4b119f41..2dae9d268 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -546,6 +546,7 @@ class ModLog(Cog, name="ModLog"): f"**Author:** {format_user(author)}\n" f"**Channel:** {channel.category}/#{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" + f"[Jump to message]({message.jump_url})\n" "\n" ) else: @@ -553,6 +554,7 @@ class ModLog(Cog, name="ModLog"): f"**Author:** {format_user(author)}\n" f"**Channel:** #{channel.name} (`{channel.id}`)\n" f"**Message ID:** `{message.id}`\n" + f"[Jump to message]({message.jump_url})\n" "\n" ) -- cgit v1.2.3 From 2fdb0e5fce6d246cfc67962e235b4d53622f03d7 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Fri, 5 Feb 2021 19:13:40 +0400 Subject: Make `KeyError` tag --- bot/resources/tags/keyerror.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 bot/resources/tags/keyerror.md diff --git a/bot/resources/tags/keyerror.md b/bot/resources/tags/keyerror.md new file mode 100644 index 000000000..d0c069004 --- /dev/null +++ b/bot/resources/tags/keyerror.md @@ -0,0 +1,17 @@ +Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary. \ +While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them. +## __The `dict.get` method__ +The [dict.get](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return the value for the key if it exists, or None (or a default value that you specify) if the key doesn't exist. Hence it will _never raise_ a KeyError. +```py +>>> my_dict = {"foo": 1, "bar": 2} +>>> print(my_dict.get("foo")) +1 +>>> print(my_dict.get("foobar")) +None +>>> print(my_dict.get("foobar", 3)) # here 3 is the default value to be returned, in case the key doesn't exist +3 +>>> print(my_dict) +{'foo': 1, 'bar': 2} # note that the new key was NOT added to the dictionary +``` +\ +Some other methods that can be used for handling KeyErrors gracefully are the [dict.setdefault](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method, or by using [collections.defaultdict](https://docs.python.org/3/library/collections.html#collections.defaultdict). -- cgit v1.2.3 From 2c4d7f41432bb620d83d3403fe4ab9317bc1129f Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Fri, 5 Feb 2021 19:39:36 +0400 Subject: Update and rename keyerror.md to dict-get.md --- bot/resources/tags/dict-get.md | 13 +++++++++++++ bot/resources/tags/keyerror.md | 17 ----------------- 2 files changed, 13 insertions(+), 17 deletions(-) create mode 100644 bot/resources/tags/dict-get.md delete mode 100644 bot/resources/tags/keyerror.md diff --git a/bot/resources/tags/dict-get.md b/bot/resources/tags/dict-get.md new file mode 100644 index 000000000..b22db7af5 --- /dev/null +++ b/bot/resources/tags/dict-get.md @@ -0,0 +1,13 @@ +Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary.\ +While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them. +__**The `dict.get` method**__ +The [`dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return the value for the key if it exists, or None (or a default value that you specify) if the key doesn't exist. Hence it will _never raise_ a KeyError. +```py +>>> my_dict = {"foo": 1, "bar": 2} +>>> print(my_dict.get("foobar")) +None +>>> print(my_dict.get("foobar", 3)) # here 3 is the default value to be returned, in case the key doesn't exist +3 +``` + +Some other methods that can be used for handling KeyErrors gracefully are the [`dict.setdefault`](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method, or by using [`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict). diff --git a/bot/resources/tags/keyerror.md b/bot/resources/tags/keyerror.md deleted file mode 100644 index d0c069004..000000000 --- a/bot/resources/tags/keyerror.md +++ /dev/null @@ -1,17 +0,0 @@ -Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary. \ -While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them. -## __The `dict.get` method__ -The [dict.get](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return the value for the key if it exists, or None (or a default value that you specify) if the key doesn't exist. Hence it will _never raise_ a KeyError. -```py ->>> my_dict = {"foo": 1, "bar": 2} ->>> print(my_dict.get("foo")) -1 ->>> print(my_dict.get("foobar")) -None ->>> print(my_dict.get("foobar", 3)) # here 3 is the default value to be returned, in case the key doesn't exist -3 ->>> print(my_dict) -{'foo': 1, 'bar': 2} # note that the new key was NOT added to the dictionary -``` -\ -Some other methods that can be used for handling KeyErrors gracefully are the [dict.setdefault](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method, or by using [collections.defaultdict](https://docs.python.org/3/library/collections.html#collections.defaultdict). -- cgit v1.2.3 From 721472352f05c561625932a88bcf3c10c98c130d Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Fri, 5 Feb 2021 19:43:29 +0400 Subject: Fix faulty heading --- bot/resources/tags/dict-get.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/resources/tags/dict-get.md b/bot/resources/tags/dict-get.md index b22db7af5..d9cc6a691 100644 --- a/bot/resources/tags/dict-get.md +++ b/bot/resources/tags/dict-get.md @@ -1,6 +1,8 @@ Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary.\ -While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them. +While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them.\ + __**The `dict.get` method**__ + The [`dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return the value for the key if it exists, or None (or a default value that you specify) if the key doesn't exist. Hence it will _never raise_ a KeyError. ```py >>> my_dict = {"foo": 1, "bar": 2} -- cgit v1.2.3 From 3a08d74e6ecc5a038ccfc97e973dac161171c6c2 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Fri, 5 Feb 2021 19:50:45 +0400 Subject: Make `defaultdict` tag --- bot/resources/tags/defaultdict.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 bot/resources/tags/defaultdict.md diff --git a/bot/resources/tags/defaultdict.md b/bot/resources/tags/defaultdict.md new file mode 100644 index 000000000..a15ebff2a --- /dev/null +++ b/bot/resources/tags/defaultdict.md @@ -0,0 +1,20 @@ +**[`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict)** + +The Python `defaultdict` type behaves almost exactly like a regular Python dictionary, but if you try to access or modify a missing key, the `defaultdict` will automatically create the key and generate a default value for it. +While instantiating a `defaultdict`, we pass in a function that tells it how to create a default value for missing keys. + +```py +>>> from collections import defaultdict +>>> my_dict = defaultdict(int, {"foo": 1, "bar": 2}) +>>> print(my_dict) +defaultdict(, {'foo': 1, 'bar': 2}) +``` + +In this example, we've used the `int` function - this means that if we try to access a non-existent key, it provides the default value of 0. + +```py +>>> print(my_dict["foobar"]) +0 +>>> print(my_dict) +defaultdict(, {'foo': 1, 'bar': 2, 'foobar': 0}) +``` -- cgit v1.2.3 From a6d65fd04e932a8cc860f5e8ab07f05a4a2f51d1 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Fri, 5 Feb 2021 19:53:49 +0400 Subject: Refer to `defaultdict` tag in `dict-get` --- bot/resources/tags/dict-get.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/dict-get.md b/bot/resources/tags/dict-get.md index d9cc6a691..6f8299dc7 100644 --- a/bot/resources/tags/dict-get.md +++ b/bot/resources/tags/dict-get.md @@ -12,4 +12,4 @@ None 3 ``` -Some other methods that can be used for handling KeyErrors gracefully are the [`dict.setdefault`](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method, or by using [`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict). +Some other methods that can be used for handling KeyErrors gracefully are the [`dict.setdefault`](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method, or by using [`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict) (check out the `!defaultdict` tag). -- cgit v1.2.3 From c346c2becd2967058a73bc900800afbfb8dbe6d5 Mon Sep 17 00:00:00 2001 From: Senjan21 <53477086+Senjan21@users.noreply.github.com> Date: Fri, 5 Feb 2021 19:12:14 +0100 Subject: Fix #1371 error via adding errors. --- bot/errors.py | 17 ++++++++++++++++- bot/exts/backend/error_handler.py | 4 +++- bot/exts/moderation/infraction/_utils.py | 5 +++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/bot/errors.py b/bot/errors.py index 65d715203..016d9bd17 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -1,4 +1,5 @@ -from typing import Hashable +from typing import Hashable, Union +from discord import Member, User class LockedResourceError(RuntimeError): @@ -18,3 +19,17 @@ class LockedResourceError(RuntimeError): f"Cannot operate on {self.type.lower()} `{self.id}`; " "it is currently locked and in use by another operation." ) + + +class InvalidInfractedUser(Exception): + """ + Exception raised upon attempt of infracting an invalid user." + + Attributes: + `user` -- User or Member which is invalid + """ + + def __init__(self, user: Union[Member, User], reason: str = "User infracted is a bot."): + self.user = user + + super().__init__(reason) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index ed7962b06..d2cce5558 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -12,7 +12,7 @@ from bot.api import ResponseCodeError from bot.bot import Bot from bot.constants import Colours, ERROR_REPLIES, Icons, MODERATION_ROLES from bot.converters import TagNameConverter -from bot.errors import LockedResourceError +from bot.errors import InvalidInfractedUser, LockedResourceError from bot.exts.backend.branding._errors import BrandingError from bot.utils.checks import InWhitelistCheckFailure @@ -82,6 +82,8 @@ class ErrorHandler(Cog): elif isinstance(e.original, BrandingError): await ctx.send(embed=self._get_error_embed(random.choice(ERROR_REPLIES), str(e.original))) return + elif isinstance(e.original, InvalidInfractedUser): + await ctx.send(f"Cannot infract that user. {e.original.reason}") else: await self.handle_unexpected_error(ctx, e.original) return # Exit early to avoid logging. diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index d0dc3f0a1..e766c1e5c 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -7,6 +7,7 @@ from discord.ext.commands import Context from bot.api import ResponseCodeError from bot.constants import Colours, Icons +from bot.errors import InvalidInfractedUser log = logging.getLogger(__name__) @@ -79,6 +80,10 @@ async def post_infraction( active: bool = True ) -> t.Optional[dict]: """Posts an infraction to the API.""" + if isinstance(user, (discord.Member, discord.User)) and user.bot: + log.trace(f"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.") + raise InvalidInfractedUser(user) + log.trace(f"Posting {infr_type} infraction for {user} to the API.") payload = { -- cgit v1.2.3 From 4f1f45652ab980502098b98e5534d64f810b4b53 Mon Sep 17 00:00:00 2001 From: Senjan21 <53477086+Senjan21@users.noreply.github.com> Date: Fri, 5 Feb 2021 19:19:02 +0100 Subject: Linting fix. --- bot/errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/errors.py b/bot/errors.py index 016d9bd17..a6fc33312 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -1,4 +1,5 @@ from typing import Hashable, Union + from discord import Member, User @@ -23,7 +24,7 @@ class LockedResourceError(RuntimeError): class InvalidInfractedUser(Exception): """ - Exception raised upon attempt of infracting an invalid user." + Exception raised upon attempt of infracting an invalid user. Attributes: `user` -- User or Member which is invalid -- cgit v1.2.3 From 78ded411d8e57d399a00d9132d4caa94ba59f410 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Fri, 5 Feb 2021 19:31:38 +0100 Subject: replaced underscore with dash --- bot/resources/tags/empty-json.md | 23 +++++++++++++++++++++++ bot/resources/tags/empty_json.md | 23 ----------------------- 2 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 bot/resources/tags/empty-json.md delete mode 100644 bot/resources/tags/empty_json.md diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md new file mode 100644 index 000000000..9e5c5fd4f --- /dev/null +++ b/bot/resources/tags/empty-json.md @@ -0,0 +1,23 @@ +When creating a new JSON file you might run into the following error. + +`JSONDecodeError: Expecting value: line 1 column 1 (char 0)` + +In short, this means that your JSON is invalid in its current state. This could very well happen because the file is just new and completely empty. +Whilst the JSON data, the data you wish to store, may be empty, the .json file must not. You most likely want to use one of the following data types in your .json file: + +``` +object +array +``` + +To resolve this issue, create one of the above data types in your .json file. It is very common to use `{}` to make an object, which works similar to a dictionary in python. +When this is added to your .json file, it will look like this: + +```json +{ + +} +``` + +The error is resolved now. +Make sure to put all your data between the `{}`, just like you would when making a dictionary. diff --git a/bot/resources/tags/empty_json.md b/bot/resources/tags/empty_json.md deleted file mode 100644 index 9e5c5fd4f..000000000 --- a/bot/resources/tags/empty_json.md +++ /dev/null @@ -1,23 +0,0 @@ -When creating a new JSON file you might run into the following error. - -`JSONDecodeError: Expecting value: line 1 column 1 (char 0)` - -In short, this means that your JSON is invalid in its current state. This could very well happen because the file is just new and completely empty. -Whilst the JSON data, the data you wish to store, may be empty, the .json file must not. You most likely want to use one of the following data types in your .json file: - -``` -object -array -``` - -To resolve this issue, create one of the above data types in your .json file. It is very common to use `{}` to make an object, which works similar to a dictionary in python. -When this is added to your .json file, it will look like this: - -```json -{ - -} -``` - -The error is resolved now. -Make sure to put all your data between the `{}`, just like you would when making a dictionary. -- cgit v1.2.3 From f0afae3c7792d3c6b9899a915a05adb95de3b45d Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Fri, 5 Feb 2021 19:49:06 +0100 Subject: Rewrite to make it more compact and to the point --- bot/resources/tags/empty-json.md | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index 9e5c5fd4f..0246d346f 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -1,23 +1,15 @@ -When creating a new JSON file you might run into the following error. -`JSONDecodeError: Expecting value: line 1 column 1 (char 0)` - -In short, this means that your JSON is invalid in its current state. This could very well happen because the file is just new and completely empty. -Whilst the JSON data, the data you wish to store, may be empty, the .json file must not. You most likely want to use one of the following data types in your .json file: +When using JSON you might run into the following error: +``` +JSONDecodeError: Expecting value: line 1 column 1 (char 0) +``` +This error could have appeared because you just created the JSON file and there is nothing in it at the moment. +Whilst having the data empty is no problem, the file itself may never be completely empty. You most likely want one of the following in your json ``` object array ``` +This issue can be resolved by creating one of these data types. An object is the most common of the 2, and is created by editing your file to read `{}`. -To resolve this issue, create one of the above data types in your .json file. It is very common to use `{}` to make an object, which works similar to a dictionary in python. -When this is added to your .json file, it will look like this: - -```json -{ - -} -``` - -The error is resolved now. -Make sure to put all your data between the `{}`, just like you would when making a dictionary. +Different data types are also supported. If you wish to read more on these, please reffer to the following article: https://www.tutorialspoint.com/json/json_data_types.htm -- cgit v1.2.3 From 68d53cbe1955b486903a7c962f8d6602766375ce Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Fri, 5 Feb 2021 19:53:16 +0100 Subject: Removed an excess line --- bot/resources/tags/empty-json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index 0246d346f..a5de2380f 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -10,6 +10,6 @@ Whilst having the data empty is no problem, the file itself may never be complet object array ``` -This issue can be resolved by creating one of these data types. An object is the most common of the 2, and is created by editing your file to read `{}`. +An object is the most common of the 2, and is created by editing your file to read `{}`. Different data types are also supported. If you wish to read more on these, please reffer to the following article: https://www.tutorialspoint.com/json/json_data_types.htm -- cgit v1.2.3 From 04b99031d9895afda5a00080c894b07958919ae9 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Fri, 5 Feb 2021 19:56:37 +0100 Subject: Fixed random newline at start --- bot/resources/tags/empty-json.md | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index a5de2380f..98bfe5fa7 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -1,4 +1,3 @@ - When using JSON you might run into the following error: ``` JSONDecodeError: Expecting value: line 1 column 1 (char 0) -- cgit v1.2.3 From 970b49aec1cfee6cdffe56b3d675224fecde382f Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Fri, 5 Feb 2021 20:47:31 +0100 Subject: Simplified language --- bot/resources/tags/empty-json.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index 98bfe5fa7..3851dc142 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -4,11 +4,8 @@ JSONDecodeError: Expecting value: line 1 column 1 (char 0) ``` This error could have appeared because you just created the JSON file and there is nothing in it at the moment. -Whilst having the data empty is no problem, the file itself may never be completely empty. You most likely want one of the following in your json -``` -object -array -``` -An object is the most common of the 2, and is created by editing your file to read `{}`. +Whilst having the data empty is no problem, the file itself may never be completely empty. + +You most likely wanted to structure your JSON as a dictionary. For this change your JSON file to read `{}`. Different data types are also supported. If you wish to read more on these, please reffer to the following article: https://www.tutorialspoint.com/json/json_data_types.htm -- cgit v1.2.3 From 81af0099ffd552aa5cb2a61de30cf7bd16a013eb Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 10:41:47 +0200 Subject: Implement showing filterlist entry comment in alerts --- bot/exts/filters/filtering.py | 63 +++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 3527bf8bb..6f1374cf4 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -2,7 +2,7 @@ import asyncio import logging import re from datetime import datetime, timedelta -from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Union +from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union import dateutil import discord.errors @@ -137,6 +137,10 @@ class Filtering(Cog): """Fetch items from the filter_list_cache.""" return self.bot.filter_list_cache[f"{list_type.upper()}.{allowed}"].keys() + def _get_filterlist_value(self, list_type: str, value: Any, *, allowed: bool) -> dict: + """Fetch one specific value from filter_list_cache.""" + return self.bot.filter_list_cache[f"{list_type.upper()}.{allowed}"][value] + @staticmethod def _expand_spoilers(text: str) -> str: """Return a string containing all interpretations of a spoilered message.""" @@ -236,7 +240,7 @@ class Filtering(Cog): # We also do not need to worry about filters that take the full message, # since all we have is an arbitrary string. if _filter["enabled"] and _filter["content_only"]: - match = await _filter["function"](result) + match, reason = await _filter["function"](result) if match: # If this is a filter (not a watchlist), we set the variable so we know @@ -245,7 +249,7 @@ class Filtering(Cog): filter_triggered = True stats = self._add_stats(filter_name, match, result) - await self._send_log(filter_name, _filter, msg, stats, is_eval=True) + await self._send_log(filter_name, _filter, msg, stats, reason, is_eval=True) break # We don't want multiple filters to trigger @@ -267,9 +271,9 @@ class Filtering(Cog): # Does the filter only need the message content or the full message? if _filter["content_only"]: - match = await _filter["function"](msg.content) + match, reason = await _filter["function"](msg.content) else: - match = await _filter["function"](msg) + match, reason = await _filter["function"](msg) if match: is_private = msg.channel.type is discord.ChannelType.private @@ -316,7 +320,7 @@ class Filtering(Cog): log.trace(f"Offensive message {msg.id} will be deleted on {delete_date}") stats = self._add_stats(filter_name, match, msg.content) - await self._send_log(filter_name, _filter, msg, stats) + await self._send_log(filter_name, _filter, msg, stats, reason) break # We don't want multiple filters to trigger @@ -326,6 +330,7 @@ class Filtering(Cog): _filter: Dict[str, Any], msg: discord.Message, stats: Stats, + reason: Optional[str] = None, *, is_eval: bool = False, ) -> None: @@ -339,6 +344,7 @@ class Filtering(Cog): ping_everyone = Filter.ping_everyone and _filter.get("ping_everyone", True) eval_msg = "using !eval " if is_eval else "" + footer = f"Entry comment: {reason}" if reason else None message = ( f"The {filter_name} {_filter['type']} was triggered by {format_user(msg.author)} " f"{channel_str} {eval_msg}with [the following message]({msg.jump_url}):\n\n" @@ -357,6 +363,7 @@ class Filtering(Cog): channel_id=Channels.mod_alerts, ping_everyone=ping_everyone, additional_embeds=stats.additional_embeds, + footer=footer, ) def _add_stats(self, name: str, match: FilterMatch, content: str) -> Stats: @@ -381,9 +388,11 @@ class Filtering(Cog): if name == "filter_invites" and match is not True: additional_embeds = [] for _, data in match.items(): + reason = f"\n**Entry comment:**\n{data['reason']}" if data.get('reason') else "" embed = discord.Embed(description=( f"**Members:**\n{data['members']}\n" f"**Active:**\n{data['active']}" + f"{reason}" )) embed.set_author(name=data["name"]) embed.set_thumbnail(url=data["icon"]) @@ -411,7 +420,7 @@ class Filtering(Cog): and not msg.author.bot # Author not a bot ) - async def _has_watch_regex_match(self, text: str) -> Union[bool, re.Match]: + async def _has_watch_regex_match(self, text: str) -> Tuple[Union[bool, re.Match], Optional[str]]: """ Return True if `text` matches any regex from `word_watchlist` or `token_watchlist` configs. @@ -429,9 +438,11 @@ class Filtering(Cog): for pattern in watchlist_patterns: match = re.search(pattern, text, flags=re.IGNORECASE) if match: - return match + return match, self._get_filterlist_value('filter_token', pattern, allowed=False)['comment'] + + return False, None - async def _has_urls(self, text: str) -> bool: + async def _has_urls(self, text: str) -> Tuple[bool, Optional[str]]: """Returns True if the text contains one of the blacklisted URLs from the config file.""" if not URL_RE.search(text): return False @@ -441,20 +452,21 @@ class Filtering(Cog): for url in domain_blacklist: if url.lower() in text: - return True + return True, self._get_filterlist_value("domain_name", url, allowed=False)["comment"] - return False + return False, None @staticmethod - async def _has_zalgo(text: str) -> bool: + async def _has_zalgo(text: str) -> Tuple[bool, None]: """ Returns True if the text contains zalgo characters. Zalgo range is \u0300 – \u036F and \u0489. + Return None as second value for compability with other filters. """ - return bool(ZALGO_RE.search(text)) + return bool(ZALGO_RE.search(text)), None - async def _has_invites(self, text: str) -> Union[dict, bool]: + async def _has_invites(self, text: str) -> Tuple[Union[dict, bool], None]: """ Checks if there's any invites in the text content that aren't in the guild whitelist. @@ -500,6 +512,10 @@ class Filtering(Cog): ) if invite_not_allowed: + reason = None + if guild_id in guild_invite_blacklist: + reason = self._get_filterlist_value("guild_invite", guild_id, allowed=False)["comment"] + guild_icon_hash = guild["icon"] guild_icon = ( "https://cdn.discordapp.com/icons/" @@ -511,13 +527,14 @@ class Filtering(Cog): "id": guild['id'], "icon": guild_icon, "members": response["approximate_member_count"], - "active": response["approximate_presence_count"] + "active": response["approximate_presence_count"], + "reason": reason } - return invite_data if invite_data else False + return invite_data if invite_data else False, None @staticmethod - async def _has_rich_embed(msg: Message) -> Union[bool, List[discord.Embed]]: + async def _has_rich_embed(msg: Message) -> Tuple[Union[bool, List[discord.Embed]], None]: """Determines if `msg` contains any rich embeds not auto-generated from a URL.""" if msg.embeds: for embed in msg.embeds: @@ -526,24 +543,24 @@ class Filtering(Cog): if not embed.url or embed.url not in urls: # If `embed.url` does not exist or if `embed.url` is not part of the content # of the message, it's unlikely to be an auto-generated embed by Discord. - return msg.embeds + return msg.embeds, None else: log.trace( "Found a rich embed sent by a regular user account, " "but it was likely just an automatic URL embed." ) - return False - return False + return False, None + return False, None @staticmethod - async def _has_everyone_ping(text: str) -> bool: + async def _has_everyone_ping(text: str) -> Tuple[bool, None]: """Determines if `msg` contains an @everyone or @here ping outside of a codeblock.""" # First pass to avoid running re.sub on every message if not EVERYONE_PING_RE.search(text): - return False + return False, None content_without_codeblocks = CODE_BLOCK_RE.sub("", text) - return bool(EVERYONE_PING_RE.search(content_without_codeblocks)) + return bool(EVERYONE_PING_RE.search(content_without_codeblocks)), None async def notify_member(self, filtered_member: Member, reason: str, channel: TextChannel) -> None: """ -- cgit v1.2.3 From 6d9e17634ac10ce911e68d544a52aaa928298929 Mon Sep 17 00:00:00 2001 From: xithrius Date: Sat, 6 Feb 2021 01:25:44 -0800 Subject: Reformatted string constant for available help channels. --- bot/exts/help_channels/_cog.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index e9333b9a6..fbfc585a4 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -21,9 +21,7 @@ NAMESPACE = "help" HELP_CHANNEL_TOPIC = """ This is a Python help channel. You can claim your own help channel in the Python Help: Available category. """ -AVAILABLE_HELP_CHANNELS = """ -**Currently available help channel(s):** {available} -""" +AVAILABLE_HELP_CHANNELS = """**Currently available help channel(s):** {available}""" class HelpChannels(commands.Cog): -- cgit v1.2.3 From bbce0a8cb17d0771a4823e67675cf4dc26f72b2a Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 11:37:34 +0200 Subject: Create local-file tag about sending local files to Discord --- bot/resources/tags/local-file.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 bot/resources/tags/local-file.md diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md new file mode 100644 index 000000000..309ca4820 --- /dev/null +++ b/bot/resources/tags/local-file.md @@ -0,0 +1,24 @@ +Thanks to discord.py, sending local files as embed images is simple. You have to create an instance of `discord.File` class: +```py +# When you know the file exact path, you can pass it. +file = discord.File("/this/is/path/to/my/file.png", filename="file.png") + +# When you have the file-like object, then you can pass this instead path. +with open("/this/is/path/to/my/file.png", "rb") as f: + file = discord.File(f) +``` +When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing filename to it is not necessary. +Please note that `filename` can't contain underscores. This is Discord limitation. + +`discord.Embed` instance has method `set_image` what can be used to set attachment as image: +```py +embed = discord.Embed() +# Set other fields +embed.set_image("attachment://file.png") # Filename here must be exactly same as attachment filename. +``` +After this, you can send embed and attachment to Discord: +```py +await channel.send(file=file, embed=embed) +``` +This example uses `discord.Channel` for sending, but any `discord.Messageable` can be used for sending. + -- cgit v1.2.3 From dc72923a4c74207fc405764a8e8afc1b4b239b37 Mon Sep 17 00:00:00 2001 From: xithrius Date: Sat, 6 Feb 2021 01:52:42 -0800 Subject: Available channels are no longer stored as IDs. --- bot/exts/help_channels/_cog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index fbfc585a4..dae9b5730 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -76,7 +76,7 @@ class HelpChannels(commands.Cog): # Acquiring and modifying the channel to dynamically update the available help channels message. self.how_to_get_help: discord.TextChannel = None - self.available_help_channels: t.Set[int] = set() + self.available_help_channels: t.Set[discord.TextChannel] = set() self.dynamic_message: discord.Message = None # Asyncio stuff @@ -122,7 +122,7 @@ class HelpChannels(commands.Cog): await _caches.unanswered.set(message.channel.id, True) # Removing the help channel from the dynamic message, and editing/sending that message. - self.available_help_channels.remove(message.channel.id) + self.available_help_channels.remove(message.channel) # Not awaited because it may indefinitely hold the lock while waiting for a channel. scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}") @@ -352,7 +352,7 @@ class HelpChannels(commands.Cog): ) # Adding the help channel to the dynamic message, and editing/sending that message. - self.available_help_channels.add(channel.id) + self.available_help_channels.add(channel) await self.update_available_help_channels() _stats.report_counts() @@ -489,11 +489,11 @@ class HelpChannels(commands.Cog): """Updates the dynamic message within #how-to-get-help for available help channels.""" if not self.available_help_channels: self.available_help_channels = set( - c.id for c in self.available_category.channels if 'help-' in c.name + c for c in self.available_category.channels if not _channel.is_excluded_channel(c) ) available_channels = AVAILABLE_HELP_CHANNELS.format( - available=', '.join(f"<#{c}>" for c in self.available_help_channels) or None + available=', '.join(c.mention for c in self.available_help_channels) or None ) if self.how_to_get_help is None: -- cgit v1.2.3 From 2b437bfc4858d6ed08eee43defd9a97584140706 Mon Sep 17 00:00:00 2001 From: xithrius Date: Sat, 6 Feb 2021 02:09:20 -0800 Subject: Removed unnecessary task creation. --- bot/exts/help_channels/_cog.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index dae9b5730..2dbe930d3 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -286,13 +286,8 @@ class HelpChannels(commands.Cog): self.close_command.enabled = True # Getting channels that need to be included in the dynamic message. - task = asyncio.create_task(self.update_available_help_channels()) - self.queue_tasks.append(task) - - await task - + await self.update_available_help_channels() log.trace("Dynamic available help message updated.") - self.queue_tasks.remove(task) await self.init_available() _stats.report_counts() -- cgit v1.2.3 From 88fba5fd3489988320431b8a96879941988b5f13 Mon Sep 17 00:00:00 2001 From: xithrius Date: Sat, 6 Feb 2021 02:21:20 -0800 Subject: Formatted available constant, added missing dynamic message trace --- bot/exts/help_channels/_cog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 2dbe930d3..554c27c95 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -21,7 +21,7 @@ NAMESPACE = "help" HELP_CHANNEL_TOPIC = """ This is a Python help channel. You can claim your own help channel in the Python Help: Available category. """ -AVAILABLE_HELP_CHANNELS = """**Currently available help channel(s):** {available}""" +AVAILABLE_HELP_CHANNELS = "**Currently available help channel(s):** {available}" class HelpChannels(commands.Cog): @@ -500,3 +500,4 @@ class HelpChannels(commands.Cog): await self.dynamic_message.edit(content=available_channels) except discord.NotFound: self.dynamic_message = await self.how_to_get_help.send(available_channels) + log.trace("A dynamic message was sent for later modification because one couldn't be found.") -- cgit v1.2.3 From d0c87c7f12ca20ec9be54bf0d299ca23a5e559db Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 12:54:45 +0200 Subject: discord.Channel -> discord.TextChannel --- bot/resources/tags/local-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index 309ca4820..a587139ee 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -20,5 +20,5 @@ After this, you can send embed and attachment to Discord: ```py await channel.send(file=file, embed=embed) ``` -This example uses `discord.Channel` for sending, but any `discord.Messageable` can be used for sending. +This example uses `discord.TextChannel` for sending, but any `discord.Messageable` can be used for sending. -- cgit v1.2.3 From 44be3e8a7411a715b502802863dfc1fb2d6658c3 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 12:56:52 +0200 Subject: discord.Messageable -> discord.abc.Messageable --- bot/resources/tags/local-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index a587139ee..d78258fa2 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -20,5 +20,5 @@ After this, you can send embed and attachment to Discord: ```py await channel.send(file=file, embed=embed) ``` -This example uses `discord.TextChannel` for sending, but any `discord.Messageable` can be used for sending. +This example uses `discord.TextChannel` for sending, but any `discord.abc.Messageable` can be used for sending. -- cgit v1.2.3 From 6db50230970188b4b1a24ec0b4ff84b4896cc78a Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 13:03:35 +0200 Subject: Remove additional newline from end of tag --- bot/resources/tags/local-file.md | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index d78258fa2..9e4e0e551 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -21,4 +21,3 @@ After this, you can send embed and attachment to Discord: await channel.send(file=file, embed=embed) ``` This example uses `discord.TextChannel` for sending, but any `discord.abc.Messageable` can be used for sending. - -- cgit v1.2.3 From 1a9d820638acce176f73867b6b321c8c1dbfb479 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 13:38:39 +0200 Subject: Ignore attachment-only messages for duplicates antispam rule --- bot/rules/duplicates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/rules/duplicates.py b/bot/rules/duplicates.py index 455764b53..23aefd3dc 100644 --- a/bot/rules/duplicates.py +++ b/bot/rules/duplicates.py @@ -13,6 +13,7 @@ async def apply( if ( msg.author == last_message.author and msg.content == last_message.content + and (msg.content and not msg.attachments) ) ) -- cgit v1.2.3 From 84a46c9ab27f0a593c413f5ee09ba19cf5fb1d1b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 13:45:07 +0200 Subject: Lower max attachments per 10 seconds to 3 --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index d3b267159..d323a946d 100644 --- a/config-default.yml +++ b/config-default.yml @@ -367,7 +367,7 @@ anti_spam: rules: attachments: interval: 10 - max: 9 + max: 3 burst: interval: 10 -- cgit v1.2.3 From e1fa3182254727c564afc86d87fc7043b2444c3c Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 13:56:50 +0200 Subject: Mention instance in comment about Messageable --- bot/resources/tags/local-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index 9e4e0e551..c28e14a05 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -20,4 +20,4 @@ After this, you can send embed and attachment to Discord: ```py await channel.send(file=file, embed=embed) ``` -This example uses `discord.TextChannel` for sending, but any `discord.abc.Messageable` can be used for sending. +This example uses `discord.TextChannel` for sending, but any instance of `discord.abc.Messageable` can be used for sending. -- cgit v1.2.3 From 90a9bac84cdac0288c256157f1b5769b0cd2b973 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 14:03:41 +0200 Subject: Add hyperlinks for local file tag discord.py references --- bot/resources/tags/local-file.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index c28e14a05..344f35667 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -1,4 +1,4 @@ -Thanks to discord.py, sending local files as embed images is simple. You have to create an instance of `discord.File` class: +Thanks to discord.py, sending local files as embed images is simple. You have to create an instance of [`discord.File`](https://discordpy.readthedocs.io/en/latest/api.html#discord.File) class: ```py # When you know the file exact path, you can pass it. file = discord.File("/this/is/path/to/my/file.png", filename="file.png") @@ -10,14 +10,14 @@ with open("/this/is/path/to/my/file.png", "rb") as f: When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing filename to it is not necessary. Please note that `filename` can't contain underscores. This is Discord limitation. -`discord.Embed` instance has method `set_image` what can be used to set attachment as image: +[`discord.Embed`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed) instance has method [`set_image`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed.set_image) what can be used to set attachment as image: ```py embed = discord.Embed() # Set other fields -embed.set_image("attachment://file.png") # Filename here must be exactly same as attachment filename. +embed.set_image(url="attachment://file.png") # Filename here must be exactly same as attachment filename. ``` After this, you can send embed and attachment to Discord: ```py await channel.send(file=file, embed=embed) ``` -This example uses `discord.TextChannel` for sending, but any instance of `discord.abc.Messageable` can be used for sending. +This example uses [`discord.TextChannel`](https://discordpy.readthedocs.io/en/latest/api.html#discord.TextChannel) for sending, but any instance of [`discord.abc.Messageable`](https://discordpy.readthedocs.io/en/latest/api.html#discord.abc.Messageable) can be used for sending. -- cgit v1.2.3 From fbdfaeafb5c3381d545657a395efec07daaea092 Mon Sep 17 00:00:00 2001 From: swfarnsworth Date: Sat, 6 Feb 2021 10:30:54 -0500 Subject: Rewrite to use simpler examples. The previous examples might have been confusing for some readers. I also removed the part about inverting a dict because I think that's out of scope and would require more explanation given all the consequences that could have. --- bot/resources/tags/dictcomps.md | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/bot/resources/tags/dictcomps.md b/bot/resources/tags/dictcomps.md index 11867d77b..c9f9e62f7 100644 --- a/bot/resources/tags/dictcomps.md +++ b/bot/resources/tags/dictcomps.md @@ -1,20 +1,15 @@ -**Dictionary Comprehensions** - -Like lists, there is a convenient way of creating dictionaries: +Dictionary comprehensions (*dict comps*) provide a convenient way to make dictionaries, just like list comps: ```py ->>> ftoc = {f: round((5/9)*(f-32)) for f in range(-40,101,20)} ->>> print(ftoc) -{-40: -40, -20: -29, 0: -18, 20: -7, 40: 4, 60: 16, 80: 27, 100: 38} +>>> {word.lower(): len(word) for word in ('I', 'love', 'Python')} +{'i': 1, 'love': 4, 'python': 6} ``` -In the example above, I created a dictionary of temperatures in Fahrenheit, that are mapped to (*roughly*) their Celsius counterpart within a small range. These comprehensions are useful for succinctly creating dictionaries from some other sequence. +The syntax is very similar to list comps except that you surround it with curly braces and have two expressions: one for the key and one for the value. -They are also very useful for inverting the key value pairs of a dictionary that already exists, such that the value in the old dictionary is now the key, and the corresponding key is now its value: +One can use a dict comp to change an existing dictionary using its `items` method ```py ->>> ctof = {v:k for k, v in ftoc.items()} ->>> print(ctof) -{-40: -40, -29: -20, -18: 0, -7: 20, 4: 40, 16: 60, 27: 80, 38: 100} +>>> first_dict = {'i': 1, 'love': 4, 'python': 6} +>>> {key.upper(): value * 2 for key, value in first_dict.items()} +{'I': 2, 'LOVE': 8, 'PYTHON': 12} ``` -Also like list comprehensions, you can add a conditional to it in order to filter out items you don't want. - -For more information and examples, check [PEP 274](https://www.python.org/dev/peps/pep-0274/) +For more information and examples, check out [PEP 274](https://www.python.org/dev/peps/pep-0274/) -- cgit v1.2.3 From 9655acb6e4b19eec9aadb5cc1b7ed76ef55aff82 Mon Sep 17 00:00:00 2001 From: swfarnsworth Date: Sat, 6 Feb 2021 10:33:36 -0500 Subject: More robust example with no reference to Python versions or `str.format`. The example emphasizes that you can evaluate expressions in the curly braces. Python 3.5 has already reached EOL, so anyone who doesn't have f-strings at this point is probably running 2.7 anyway. I also removed the information about `str.format` to reduce the scope. --- bot/resources/tags/f-strings.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/bot/resources/tags/f-strings.md b/bot/resources/tags/f-strings.md index 69bc82487..4f12640aa 100644 --- a/bot/resources/tags/f-strings.md +++ b/bot/resources/tags/f-strings.md @@ -1,17 +1,9 @@ -In Python, there are several ways to do string interpolation, including using `%s`\'s and by using the `+` operator to concatenate strings together. However, because some of these methods offer poor readability and require typecasting to prevent errors, you should for the most part be using a feature called format strings. +Creating a Python string with your variables using the `+` operator can be difficult to write and read. F-strings (*format-strings*) make it easy to insert values into a string. If you put an `f` in front of the first quote, you can then put Python expressions between curly braces in the string. -**In Python 3.6 or later, we can use f-strings like this:** ```py -snake = "Pythons" -print(f"{snake} are some of the largest snakes in the world") -``` -**In earlier versions of Python or in projects where backwards compatibility is very important, use str.format() like this:** -```py -snake = "Pythons" - -# With str.format() you can either use indexes -print("{0} are some of the largest snakes in the world".format(snake)) - -# Or keyword arguments -print("{family} are some of the largest snakes in the world".format(family=snake)) +>>> snake = "pythons" +>>> number = 21 +>>> f"There are {number * 2} {snake} on the plane." +"There are 42 pythons on the plane." ``` +Note that even when you include an expression that isn't a string, like `number * 2`, Python will handle converting it to a string. -- cgit v1.2.3 From d333a777aff579ac9d4f38467345fb946dd46bc3 Mon Sep 17 00:00:00 2001 From: swfarnsworth Date: Sat, 6 Feb 2021 10:35:27 -0500 Subject: New example to emphasize the mapping functionality rather than filtering. Previously, the example only conveyed how the `if` statement of list comps could be used to filter a list, whereas the mapping functionality is what people primarily use list comps for. --- bot/resources/tags/listcomps.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/bot/resources/tags/listcomps.md b/bot/resources/tags/listcomps.md index 0003b9bb8..ba00a4bf7 100644 --- a/bot/resources/tags/listcomps.md +++ b/bot/resources/tags/listcomps.md @@ -1,14 +1,19 @@ -Do you ever find yourself writing something like: +Do you ever find yourself writing something like this? ```py -even_numbers = [] -for n in range(20): - if n % 2 == 0: - even_numbers.append(n) +>>> squares = [] +>>> for n in range(5): +... squares.append(n ** 2) +[0, 1, 4, 9, 16] ``` -Using list comprehensions can simplify this significantly, and greatly improve code readability. If we rewrite the example above to use list comprehensions, it would look like this: +Using list comprehensions can make this both shorter and more readable. As a list comprehension, the same code would look like this: ```py -even_numbers = [n for n in range(20) if n % 2 == 0] +>>> [n ** 2 for n in range(5)] +[0, 1, 4, 9, 16] +``` +List comprehensions also get an `if` statement: +```python +>>> [n ** 2 for n in range(5) if n % 2 == 0] +[0, 4, 16] ``` -This also works for generators, dicts and sets by using `()` or `{}` instead of `[]`. -For more info, see [this pythonforbeginners.com post](http://www.pythonforbeginners.com/basics/list-comprehensions-in-python) or [PEP 202](https://www.python.org/dev/peps/pep-0202/). +For more info, see [this pythonforbeginners.com post](http://www.pythonforbeginners.com/basics/list-comprehensions-in-python). -- cgit v1.2.3 From 8f51f239f2ded1d7176a94039d2332ef74532a95 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 18:07:07 +0200 Subject: Fix grammar of local-file tag --- bot/resources/tags/local-file.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index 344f35667..52539c64e 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -10,13 +10,13 @@ with open("/this/is/path/to/my/file.png", "rb") as f: When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing filename to it is not necessary. Please note that `filename` can't contain underscores. This is Discord limitation. -[`discord.Embed`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed) instance has method [`set_image`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed.set_image) what can be used to set attachment as image: +[`discord.Embed`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed) instances have a [`set_image`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed.set_image) method which can be used to set an attachment as an image: ```py embed = discord.Embed() # Set other fields embed.set_image(url="attachment://file.png") # Filename here must be exactly same as attachment filename. ``` -After this, you can send embed and attachment to Discord: +After this, you send an embed with an attachment to Discord: ```py await channel.send(file=file, embed=embed) ``` -- cgit v1.2.3 From 496129080733096ab7eddd03128750b9fd3a53a2 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 18:17:00 +0200 Subject: Add back removed 'can' to local-file tag --- bot/resources/tags/local-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index 52539c64e..a4aeee736 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -16,7 +16,7 @@ embed = discord.Embed() # Set other fields embed.set_image(url="attachment://file.png") # Filename here must be exactly same as attachment filename. ``` -After this, you send an embed with an attachment to Discord: +After this, you can send an embed with an attachment to Discord: ```py await channel.send(file=file, embed=embed) ``` -- cgit v1.2.3 From 65ea1657de016a3ba1e58412950ae4bf735bf0fe Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 18:19:40 +0200 Subject: Add missing 'a' article in local-file tag --- bot/resources/tags/local-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index a4aeee736..29fce3ff5 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -8,7 +8,7 @@ with open("/this/is/path/to/my/file.png", "rb") as f: file = discord.File(f) ``` When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing filename to it is not necessary. -Please note that `filename` can't contain underscores. This is Discord limitation. +Please note that `filename` can't contain underscores. This is a Discord limitation.. [`discord.Embed`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed) instances have a [`set_image`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed.set_image) method which can be used to set an attachment as an image: ```py -- cgit v1.2.3 From 691f2393f0fed5d17ec641d5006ea2e486015614 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 18:21:55 +0200 Subject: Remove unnecessary period from local-file tag --- bot/resources/tags/local-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index 29fce3ff5..fdce5605c 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -8,7 +8,7 @@ with open("/this/is/path/to/my/file.png", "rb") as f: file = discord.File(f) ``` When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing filename to it is not necessary. -Please note that `filename` can't contain underscores. This is a Discord limitation.. +Please note that `filename` can't contain underscores. This is a Discord limitation. [`discord.Embed`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed) instances have a [`set_image`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed.set_image) method which can be used to set an attachment as an image: ```py -- cgit v1.2.3 From 174f70e216e327e30a9df6902619944f47eea5ad Mon Sep 17 00:00:00 2001 From: Senjan21 <53477086+Senjan21@users.noreply.github.com> Date: Sat, 6 Feb 2021 18:10:17 +0100 Subject: add reason attribute --- bot/errors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/errors.py b/bot/errors.py index a6fc33312..ab0adcd42 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -32,5 +32,6 @@ class InvalidInfractedUser(Exception): def __init__(self, user: Union[Member, User], reason: str = "User infracted is a bot."): self.user = user + self.reason = reason super().__init__(reason) -- cgit v1.2.3 From 255f2215279d08386152b12dc8adec037538cba7 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Sat, 6 Feb 2021 21:16:23 +0400 Subject: Reword comment in example --- bot/resources/tags/dict-get.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/dict-get.md b/bot/resources/tags/dict-get.md index 6f8299dc7..7657f420a 100644 --- a/bot/resources/tags/dict-get.md +++ b/bot/resources/tags/dict-get.md @@ -1,5 +1,5 @@ Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary.\ -While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them.\ +While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them. __**The `dict.get` method**__ @@ -8,7 +8,7 @@ The [`dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) metho >>> my_dict = {"foo": 1, "bar": 2} >>> print(my_dict.get("foobar")) None ->>> print(my_dict.get("foobar", 3)) # here 3 is the default value to be returned, in case the key doesn't exist +>>> print(my_dict.get("foobar", 3)) # here 3 is the default value to be returned, because the key doesn't exist 3 ``` -- cgit v1.2.3 From df22551dbf7a4dae4e374eb1dd95d9354b73474c Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Sat, 6 Feb 2021 21:41:13 +0400 Subject: Fix trailing whitespaces --- bot/resources/tags/defaultdict.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/defaultdict.md b/bot/resources/tags/defaultdict.md index a15ebff2a..9361d6f2a 100644 --- a/bot/resources/tags/defaultdict.md +++ b/bot/resources/tags/defaultdict.md @@ -1,6 +1,6 @@ **[`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict)** -The Python `defaultdict` type behaves almost exactly like a regular Python dictionary, but if you try to access or modify a missing key, the `defaultdict` will automatically create the key and generate a default value for it. +The Python `defaultdict` type behaves almost exactly like a regular Python dictionary, but if you try to access or modify a missing key, the `defaultdict` will automatically create the key and generate a default value for it. While instantiating a `defaultdict`, we pass in a function that tells it how to create a default value for missing keys. ```py -- cgit v1.2.3 From 5dc8f0e7e0cf150a9a89787b518fdbb7f8f2ba5c Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Sat, 6 Feb 2021 23:28:04 +0400 Subject: Correct examples, reword description --- bot/resources/tags/defaultdict.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bot/resources/tags/defaultdict.md b/bot/resources/tags/defaultdict.md index 9361d6f2a..b6c3175fc 100644 --- a/bot/resources/tags/defaultdict.md +++ b/bot/resources/tags/defaultdict.md @@ -1,20 +1,21 @@ **[`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict)** -The Python `defaultdict` type behaves almost exactly like a regular Python dictionary, but if you try to access or modify a missing key, the `defaultdict` will automatically create the key and generate a default value for it. +The Python `defaultdict` type behaves almost exactly like a regular Python dictionary, but if you try to access or modify a missing key, the `defaultdict` will automatically insert the key and generate a default value for it. While instantiating a `defaultdict`, we pass in a function that tells it how to create a default value for missing keys. ```py >>> from collections import defaultdict ->>> my_dict = defaultdict(int, {"foo": 1, "bar": 2}) ->>> print(my_dict) -defaultdict(, {'foo': 1, 'bar': 2}) +>>> my_dict = defaultdict(int) +>>> my_dict +defaultdict(, {}) ``` -In this example, we've used the `int` function - this means that if we try to access a non-existent key, it provides the default value of 0. +In this example, we've used the `int` class which returns 0 when called like a function, so any missing key will get a default value of 0. You can also get an empty list by default with `list` or an empty string with `str`. ```py ->>> print(my_dict["foobar"]) +>>> my_dict["foo"] 0 ->>> print(my_dict) -defaultdict(, {'foo': 1, 'bar': 2, 'foobar': 0}) +>>> my_dict["bar"] += 5 +>>> my_dict +defaultdict(, {'foo': 0, 'bar': 5}) ``` -- cgit v1.2.3 From fc451e1b3c375a73b000cea21f822b7f95d900d7 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 6 Feb 2021 22:04:10 +0200 Subject: Put filename between backticks --- bot/resources/tags/local-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/local-file.md b/bot/resources/tags/local-file.md index fdce5605c..ae41d589c 100644 --- a/bot/resources/tags/local-file.md +++ b/bot/resources/tags/local-file.md @@ -7,7 +7,7 @@ file = discord.File("/this/is/path/to/my/file.png", filename="file.png") with open("/this/is/path/to/my/file.png", "rb") as f: file = discord.File(f) ``` -When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing filename to it is not necessary. +When using the file-like object, you have to open it in `rb` mode. Also, in this case, passing `filename` to it is not necessary. Please note that `filename` can't contain underscores. This is a Discord limitation. [`discord.Embed`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed) instances have a [`set_image`](https://discordpy.readthedocs.io/en/latest/api.html#discord.Embed.set_image) method which can be used to set an attachment as an image: -- cgit v1.2.3 From 90eeeb046b392c1b770c44b766dd2ce78816b8bb Mon Sep 17 00:00:00 2001 From: Steele Farnsworth <32915757+swfarnsworth@users.noreply.github.com> Date: Sat, 6 Feb 2021 16:51:35 -0500 Subject: Removed extra blank line. It added more vertical white space than was wanted. Co-authored-by: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> --- bot/resources/tags/dictcomps.md | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/resources/tags/dictcomps.md b/bot/resources/tags/dictcomps.md index c9f9e62f7..6c8018761 100644 --- a/bot/resources/tags/dictcomps.md +++ b/bot/resources/tags/dictcomps.md @@ -11,5 +11,4 @@ One can use a dict comp to change an existing dictionary using its `items` metho >>> {key.upper(): value * 2 for key, value in first_dict.items()} {'I': 2, 'LOVE': 8, 'PYTHON': 12} ``` - For more information and examples, check out [PEP 274](https://www.python.org/dev/peps/pep-0274/) -- cgit v1.2.3 From 9a9eb8fc6f62ac8527f08cba6f72537c13522291 Mon Sep 17 00:00:00 2001 From: Steele Farnsworth <32915757+swfarnsworth@users.noreply.github.com> Date: Sat, 6 Feb 2021 16:52:36 -0500 Subject: "handle converting" -> "convert ... for you". Per Gustav's suggestion. Co-authored-by: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> --- bot/resources/tags/f-strings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/f-strings.md b/bot/resources/tags/f-strings.md index 4f12640aa..5ccafe723 100644 --- a/bot/resources/tags/f-strings.md +++ b/bot/resources/tags/f-strings.md @@ -6,4 +6,4 @@ Creating a Python string with your variables using the `+` operator can be diffi >>> f"There are {number * 2} {snake} on the plane." "There are 42 pythons on the plane." ``` -Note that even when you include an expression that isn't a string, like `number * 2`, Python will handle converting it to a string. +Note that even when you include an expression that isn't a string, like `number * 2`, Python will convert it to a string for you. -- cgit v1.2.3 From 1cb760e158259264bc9cf575a609bd2b6e64d1f3 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 7 Feb 2021 15:14:54 +0300 Subject: Revert "Dynamic available help channels message" --- bot/constants.py | 1 - bot/exts/help_channels/_cog.py | 40 ---------------------------------------- config-default.yml | 1 - 3 files changed, 42 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 6b86d33a3..95e22513f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -406,7 +406,6 @@ class Channels(metaclass=YAMLGetter): meta: int python_general: int - how_to_get_help: int cooldown: int diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 554c27c95..0995c8a79 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -11,7 +11,6 @@ from discord.ext import commands from bot import constants from bot.bot import Bot -from bot.constants import Channels from bot.exts.help_channels import _caches, _channel, _cooldown, _message, _name, _stats from bot.utils import channel as channel_utils, lock, scheduling @@ -21,7 +20,6 @@ NAMESPACE = "help" HELP_CHANNEL_TOPIC = """ This is a Python help channel. You can claim your own help channel in the Python Help: Available category. """ -AVAILABLE_HELP_CHANNELS = "**Currently available help channel(s):** {available}" class HelpChannels(commands.Cog): @@ -74,11 +72,6 @@ class HelpChannels(commands.Cog): self.last_notification: t.Optional[datetime] = None - # Acquiring and modifying the channel to dynamically update the available help channels message. - self.how_to_get_help: discord.TextChannel = None - self.available_help_channels: t.Set[discord.TextChannel] = set() - self.dynamic_message: discord.Message = None - # Asyncio stuff self.queue_tasks: t.List[asyncio.Task] = [] self.init_task = self.bot.loop.create_task(self.init_cog()) @@ -121,9 +114,6 @@ class HelpChannels(commands.Cog): await _caches.unanswered.set(message.channel.id, True) - # Removing the help channel from the dynamic message, and editing/sending that message. - self.available_help_channels.remove(message.channel) - # Not awaited because it may indefinitely hold the lock while waiting for a channel. scheduling.create_task(self.move_to_available(), name=f"help_claim_{message.id}") @@ -285,10 +275,6 @@ class HelpChannels(commands.Cog): # This may confuse users. So would potentially long delays for the cog to become ready. self.close_command.enabled = True - # Getting channels that need to be included in the dynamic message. - await self.update_available_help_channels() - log.trace("Dynamic available help message updated.") - await self.init_available() _stats.report_counts() @@ -346,10 +332,6 @@ class HelpChannels(commands.Cog): category_id=constants.Categories.help_available, ) - # Adding the help channel to the dynamic message, and editing/sending that message. - self.available_help_channels.add(channel) - await self.update_available_help_channels() - _stats.report_counts() async def move_to_dormant(self, channel: discord.TextChannel) -> None: @@ -479,25 +461,3 @@ class HelpChannels(commands.Cog): self.queue_tasks.remove(task) return channel - - async def update_available_help_channels(self) -> None: - """Updates the dynamic message within #how-to-get-help for available help channels.""" - if not self.available_help_channels: - self.available_help_channels = set( - c for c in self.available_category.channels if not _channel.is_excluded_channel(c) - ) - - available_channels = AVAILABLE_HELP_CHANNELS.format( - available=', '.join(c.mention for c in self.available_help_channels) or None - ) - - if self.how_to_get_help is None: - self.how_to_get_help = await channel_utils.try_get_channel(Channels.how_to_get_help) - - try: - if self.dynamic_message is None: - self.dynamic_message = await self.how_to_get_help.fetch_message(self.how_to_get_help.last_message_id) - await self.dynamic_message.edit(content=available_channels) - except discord.NotFound: - self.dynamic_message = await self.how_to_get_help.send(available_channels) - log.trace("A dynamic message was sent for later modification because one couldn't be found.") diff --git a/config-default.yml b/config-default.yml index fc1f3b3a8..d3b267159 100644 --- a/config-default.yml +++ b/config-default.yml @@ -159,7 +159,6 @@ guild: # Python Help: Available cooldown: 720603994149486673 - how_to_get_help: 704250143020417084 # Topical discord_py: 343944376055103488 -- cgit v1.2.3 From 16f8fd31b3cd321e4ac7d6eeb0ba20eeb8c78892 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers Date: Tue, 9 Feb 2021 10:53:51 +0100 Subject: Tiny grammar edit --- bot/resources/tags/empty-json.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index 3851dc142..ceb8c6eae 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -1,4 +1,4 @@ -When using JSON you might run into the following error: +When using JSON, you might run into the following error: ``` JSONDecodeError: Expecting value: line 1 column 1 (char 0) ``` @@ -6,6 +6,6 @@ This error could have appeared because you just created the JSON file and there Whilst having the data empty is no problem, the file itself may never be completely empty. -You most likely wanted to structure your JSON as a dictionary. For this change your JSON file to read `{}`. +You most likely wanted to structure your JSON as a dictionary. To do this, change your JSON to read `{}`. Different data types are also supported. If you wish to read more on these, please reffer to the following article: https://www.tutorialspoint.com/json/json_data_types.htm -- cgit v1.2.3 From 8d1a46c1866c12b719b991719c84a6c1d6f25bb4 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Tue, 9 Feb 2021 11:08:06 +0100 Subject: A small typo Co-authored-by: Kieran Siek --- bot/resources/tags/empty-json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index ceb8c6eae..21b0860c7 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -8,4 +8,4 @@ Whilst having the data empty is no problem, the file itself may never be complet You most likely wanted to structure your JSON as a dictionary. To do this, change your JSON to read `{}`. -Different data types are also supported. If you wish to read more on these, please reffer to the following article: https://www.tutorialspoint.com/json/json_data_types.htm +Different data types are also supported. If you wish to read more on these, please refer to the following article: https://www.tutorialspoint.com/json/json_data_types.htm -- cgit v1.2.3 From 2627bc98da2c71a6a10a6b7039522d1938c08552 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Tue, 9 Feb 2021 11:29:09 +0100 Subject: Hyperlink URL Suggestion of @Numelor Co-authored-by: Numerlor <25886452+Numerlor@users.noreply.github.com> --- bot/resources/tags/empty-json.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index 21b0860c7..93e2cadba 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -8,4 +8,4 @@ Whilst having the data empty is no problem, the file itself may never be complet You most likely wanted to structure your JSON as a dictionary. To do this, change your JSON to read `{}`. -Different data types are also supported. If you wish to read more on these, please refer to the following article: https://www.tutorialspoint.com/json/json_data_types.htm +Different data types are also supported. If you wish to read more on these, please refer to [this article](https://www.tutorialspoint.com/json/json_data_types.htm). -- cgit v1.2.3 From 160bf89303436e3ba0ff566241a206a120a25d66 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 9 Feb 2021 13:37:28 +0300 Subject: Moves Off Topic Name Translator Breaks out the off topic name translation functionality into its own function. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/converters.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/bot/converters.py b/bot/converters.py index 0d9a519df..80ce99459 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -357,27 +357,38 @@ class Duration(DurationDelta): class OffTopicName(Converter): """A converter that ensures an added off-topic name is valid.""" + ALLOWED_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-" + + @classmethod + def translate_name(cls, name: str, *, from_unicode: bool = True) -> str: + """ + Translates `name` into a format that is allowed in discord channel names. + + If `from_unicode` is True, the name is translated from a discord-safe format, back to normalized text. + """ + if from_unicode: + table = str.maketrans(cls.ALLOWED_CHARACTERS, '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹ǃ?’’-') + else: + table = str.maketrans('𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹ǃ?’’-', cls.ALLOWED_CHARACTERS) + + return name.translate(table) + async def convert(self, ctx: Context, argument: str) -> str: """Attempt to replace any invalid characters with their approximate Unicode equivalent.""" - allowed_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!?'`-" - # Chain multiple words to a single one argument = "-".join(argument.split()) if not (2 <= len(argument) <= 96): raise BadArgument("Channel name must be between 2 and 96 chars long") - elif not all(c.isalnum() or c in allowed_characters for c in argument): + elif not all(c.isalnum() or c in self.ALLOWED_CHARACTERS for c in argument): raise BadArgument( "Channel name must only consist of " "alphanumeric characters, minus signs or apostrophes." ) # Replace invalid characters with unicode alternatives. - table = str.maketrans( - allowed_characters, '𝖠𝖡𝖢𝖣𝖤𝖥𝖦𝖧𝖨𝖩𝖪𝖫𝖬𝖭𝖮𝖯𝖰𝖱𝖲𝖳𝖴𝖵𝖶𝖷𝖸𝖹ǃ?’’-' - ) - return argument.translate(table) + return self.translate_name(argument) class ISODateTime(Converter): -- cgit v1.2.3 From 66cda4fd2a0b26e2f9e983f1597a15bfb9527143 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 9 Feb 2021 13:38:12 +0300 Subject: Makes Off Topic Name Search Case Insensitive Modifies the off topic channel name search to match upper and lower cased letters, as well as punctuation. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/fun/off_topic_names.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/bot/exts/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py index 7fc93b88c..845b8175c 100644 --- a/bot/exts/fun/off_topic_names.py +++ b/bot/exts/fun/off_topic_names.py @@ -139,10 +139,20 @@ class OffTopicNames(Cog): @has_any_role(*MODERATION_ROLES) async def search_command(self, ctx: Context, *, query: OffTopicName) -> None: """Search for an off-topic name.""" - result = await self.bot.api_client.get('bot/off-topic-channel-names') - in_matches = {name for name in result if query in name} - close_matches = difflib.get_close_matches(query, result, n=10, cutoff=0.70) - lines = sorted(f"• {name}" for name in in_matches.union(close_matches)) + query = OffTopicName.translate_name(query, from_unicode=False).lower() + + # Map normalized names to returned names for search purposes + result = { + OffTopicName.translate_name(name, from_unicode=False).lower(): name + for name in await self.bot.api_client.get('bot/off-topic-channel-names') + } + + # Search normalized keys + in_matches = {name for name in result.keys() if query in name} + close_matches = difflib.get_close_matches(query, result.keys(), n=10, cutoff=0.70) + + # Send Results + lines = sorted(f"• {result[name]}" for name in in_matches.union(close_matches)) embed = Embed( title="Query results", colour=Colour.blue() -- cgit v1.2.3 From 9d8162a688023a3b5e830057b09c2ab2e132582f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 10 Feb 2021 01:07:43 +0000 Subject: Migrate API utilities to use internal DNS routing --- bot/api.py | 2 +- bot/constants.py | 1 + config-default.yml | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/api.py b/bot/api.py index d93f9f2ba..6ce9481f4 100644 --- a/bot/api.py +++ b/bot/api.py @@ -53,7 +53,7 @@ class APIClient: @staticmethod def _url_for(endpoint: str) -> str: - return f"{URLs.site_schema}{URLs.site_api}/{quote_url(endpoint)}" + return f"{URLs.site_api_schema}{URLs.site_api}/{quote_url(endpoint)}" async def close(self) -> None: """Close the aiohttp session.""" diff --git a/bot/constants.py b/bot/constants.py index 95e22513f..91e41e334 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -530,6 +530,7 @@ class URLs(metaclass=YAMLGetter): site: str site_api: str site_schema: str + site_api_schema: str # Site endpoints site_logs_view: str diff --git a/config-default.yml b/config-default.yml index d3b267159..c585151c9 100644 --- a/config-default.yml +++ b/config-default.yml @@ -335,9 +335,10 @@ keys: urls: # PyDis site vars site: &DOMAIN "pythondiscord.com" - site_api: &API !JOIN ["api.", *DOMAIN] + site_api: &API "pydis-api.default.svc.cluster.local" site_paste: &PASTE !JOIN ["paste.", *DOMAIN] site_schema: &SCHEMA "https://" + site_api_schema: "http://" site_staff: &STAFF !JOIN ["staff.", *DOMAIN] paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] -- cgit v1.2.3 From 578a0e48514fd9f902cde45db557fa1f3425c289 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 10 Feb 2021 01:07:54 +0000 Subject: Migrate ping command to ping internal API --- bot/exts/utils/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index 572fc934b..e62811b91 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -37,7 +37,7 @@ class Latency(commands.Cog): bot_ping = f"{bot_ping:.{ROUND_LATENCY}f} ms" try: - delay = await aioping.ping(URLs.site, family=socket.AddressFamily.AF_INET) * 1000 + delay = await aioping.ping(URLs.site_api, family=socket.AddressFamily.AF_INET) * 1000 site_ping = f"{delay:.{ROUND_LATENCY}f} ms" except TimeoutError: -- cgit v1.2.3 From 9111f9f247fb1292add29929c660e9633e4f31da Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 10 Feb 2021 01:57:00 +0000 Subject: Alphabetical sorting in config-default.yml --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index c585151c9..d7415c821 100644 --- a/config-default.yml +++ b/config-default.yml @@ -336,9 +336,9 @@ urls: # PyDis site vars site: &DOMAIN "pythondiscord.com" site_api: &API "pydis-api.default.svc.cluster.local" + site_api_schema: "http://" site_paste: &PASTE !JOIN ["paste.", *DOMAIN] site_schema: &SCHEMA "https://" - site_api_schema: "http://" site_staff: &STAFF !JOIN ["staff.", *DOMAIN] paste_service: !JOIN [*SCHEMA, *PASTE, "/{key}"] -- cgit v1.2.3 From bafa6a9dbf61ae30ef235537408f0b073a88dd19 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Wed, 10 Feb 2021 02:13:42 +0000 Subject: ICMP is disabled in production, so we can't ping the API --- bot/exts/utils/ping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index e62811b91..572fc934b 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -37,7 +37,7 @@ class Latency(commands.Cog): bot_ping = f"{bot_ping:.{ROUND_LATENCY}f} ms" try: - delay = await aioping.ping(URLs.site_api, family=socket.AddressFamily.AF_INET) * 1000 + delay = await aioping.ping(URLs.site, family=socket.AddressFamily.AF_INET) * 1000 site_ping = f"{delay:.{ROUND_LATENCY}f} ms" except TimeoutError: -- cgit v1.2.3 From ea35aa9c77a81f46ea14acf36862c42f3ffe9016 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Thu, 11 Feb 2021 09:37:33 +0400 Subject: Split example codeblock in two --- bot/resources/tags/dict-get.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bot/resources/tags/dict-get.md b/bot/resources/tags/dict-get.md index 7657f420a..867f0b7d9 100644 --- a/bot/resources/tags/dict-get.md +++ b/bot/resources/tags/dict-get.md @@ -1,15 +1,17 @@ Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary.\ While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them. -__**The `dict.get` method**__ +**The `dict.get` method** -The [`dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return the value for the key if it exists, or None (or a default value that you specify) if the key doesn't exist. Hence it will _never raise_ a KeyError. +The [`dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get) method will return the value for the key if it exists, and None (or a default value that you specify) if the key doesn't exist. Hence it will _never raise_ a KeyError. ```py >>> my_dict = {"foo": 1, "bar": 2} >>> print(my_dict.get("foobar")) None ->>> print(my_dict.get("foobar", 3)) # here 3 is the default value to be returned, because the key doesn't exist +``` +Below, 3 is the default value to be returned, because the key doesn't exist- +```py +>>> print(my_dict.get("foobar", 3)) 3 ``` - -Some other methods that can be used for handling KeyErrors gracefully are the [`dict.setdefault`](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method, or by using [`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict) (check out the `!defaultdict` tag). +Some other methods for handling `KeyError`s gracefully are the [`dict.setdefault`](https://docs.python.org/3/library/stdtypes.html#dict.setdefault) method and [`collections.defaultdict`](https://docs.python.org/3/library/collections.html#collections.defaultdict) (check out the `!defaultdict` tag). -- cgit v1.2.3 From a3749786c3ff90397427032ef219b590ee4e2837 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Thu, 11 Feb 2021 10:20:06 +0400 Subject: Remove reference to `try - except` --- bot/resources/tags/dict-get.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/resources/tags/dict-get.md b/bot/resources/tags/dict-get.md index 867f0b7d9..e02df03ab 100644 --- a/bot/resources/tags/dict-get.md +++ b/bot/resources/tags/dict-get.md @@ -1,5 +1,4 @@ -Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary.\ -While you can use a `try` and `except` block to catch the `KeyError`, Python also gives you some other neat ways to handle them. +Often while using dictionaries in Python, you may run into `KeyErrors`. This error is raised when you try to access a key that isn't present in your dictionary. Python gives you some neat ways to handle them. **The `dict.get` method** -- cgit v1.2.3 From 84c0aa4268f91027cd71016e01a00ffe59151cc2 Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 11 Feb 2021 02:40:42 -0800 Subject: Added base of the pypi command. --- bot/exts/info/pypi.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 bot/exts/info/pypi.py diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py new file mode 100644 index 000000000..9567516c2 --- /dev/null +++ b/bot/exts/info/pypi.py @@ -0,0 +1,35 @@ +from discord import Embed +from discord.ext.commands import Cog, Context, command + +from bot.bot import Bot +from bot.constants import NEGATIVE_REPLIES + +URL = "https://pypi.org/pypi/{package}/json" + + +class PyPi(Cog): + """Cog for getting information about PyPi packages.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @command(name="pypi", aliases=("package", "pack")) + async def get_package_info(self, ctx: Context, package: str) -> None: + """Getting information about a specific package.""" + embed = Embed(title="PyPi package information") + + async with self.bot.http_session.get(URL.format(package_name=package)) as response: + if response.status == 404: + return await ctx.send(f"Package with name '{package}' could not be found.") + elif response.status == 200 and response.content_type == "application/json": + response_json = await response.json() + info = response_json["info"] + else: + return await ctx.send("There was an error when fetching your PyPi package.") + + await ctx.send(embed=embed) + + +def setup(bot: Bot) -> None: + """Load the PyPi cog.""" + bot.add_cog(PyPi(bot)) -- cgit v1.2.3 From 1610f330fbc583df2c161629b7d8d72b77b9253d Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 11 Feb 2021 03:49:59 -0800 Subject: Added more fields and responses. --- bot/exts/info/pypi.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 9567516c2..e4c90090d 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -1,10 +1,16 @@ +import logging +from random import choice + from discord import Embed from discord.ext.commands import Cog, Context, command from bot.bot import Bot -from bot.constants import NEGATIVE_REPLIES +from bot.constants import NEGATIVE_REPLIES, Colours URL = "https://pypi.org/pypi/{package}/json" +FIELDS = ["author", "requires_python", "description", "license"] + +log = logging.getLogger(__name__) class PyPi(Cog): @@ -16,16 +22,30 @@ class PyPi(Cog): @command(name="pypi", aliases=("package", "pack")) async def get_package_info(self, ctx: Context, package: str) -> None: """Getting information about a specific package.""" - embed = Embed(title="PyPi package information") + embed = Embed(title=choice(NEGATIVE_REPLIES), colour=Colours.soft_red) - async with self.bot.http_session.get(URL.format(package_name=package)) as response: + async with self.bot.http_session.get(URL.format(package=package)) as response: if response.status == 404: - return await ctx.send(f"Package with name '{package}' could not be found.") + embed.description = f"Package could not be found." + elif response.status == 200 and response.content_type == "application/json": response_json = await response.json() info = response_json["info"] + + embed.title = "Python Package Index" + embed.colour = Colours.soft_green + embed.description = f"[{info['name']} v{info['version']}]({info['download_url']})\n" + + for field in FIELDS: + embed.add_field( + name=field.replace("_", " ").title(), + value=info[field], + inline=False, + ) + else: - return await ctx.send("There was an error when fetching your PyPi package.") + embed.description = "There was an error when fetching your PyPi package." + log.trace(f"Error when fetching PyPi package: {response.status}.") await ctx.send(embed=embed) -- cgit v1.2.3 From ded34bc8ab063064fbd50199d07bbeec1db884ad Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 11 Feb 2021 03:54:40 -0800 Subject: Made flake8 very happy. --- bot/exts/info/pypi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index e4c90090d..544b52b49 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -5,7 +5,7 @@ from discord import Embed from discord.ext.commands import Cog, Context, command from bot.bot import Bot -from bot.constants import NEGATIVE_REPLIES, Colours +from bot.constants import Colours, NEGATIVE_REPLIES URL = "https://pypi.org/pypi/{package}/json" FIELDS = ["author", "requires_python", "description", "license"] @@ -26,7 +26,7 @@ class PyPi(Cog): async with self.bot.http_session.get(URL.format(package=package)) as response: if response.status == 404: - embed.description = f"Package could not be found." + embed.description = "Package could not be found." elif response.status == 200 and response.content_type == "application/json": response_json = await response.json() -- cgit v1.2.3 From ed7fde738db677ced25388a53ed9bd539f4490fb Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 12 Feb 2021 00:14:48 -0800 Subject: Empty fields have been accounted for by getting usually non-empty ones. --- bot/exts/info/pypi.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 544b52b49..7a5d7f4b7 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -8,7 +8,7 @@ from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES URL = "https://pypi.org/pypi/{package}/json" -FIELDS = ["author", "requires_python", "description", "license"] +FIELDS = ["author", "requires_python", "summary", "license"] log = logging.getLogger(__name__) @@ -34,14 +34,15 @@ class PyPi(Cog): embed.title = "Python Package Index" embed.colour = Colours.soft_green - embed.description = f"[{info['name']} v{info['version']}]({info['download_url']})\n" + embed.description = f"[{info['name']} v{info['version']}]({info['package_url']})\n" for field in FIELDS: - embed.add_field( - name=field.replace("_", " ").title(), - value=info[field], - inline=False, - ) + if field_value := info[field]: + embed.add_field( + name=field.replace("_", " ").title(), + value=field_value, + inline=False, + ) else: embed.description = "There was an error when fetching your PyPi package." -- cgit v1.2.3 From 2a9f349429694d48cca86af972ef327a57af552d Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 12 Feb 2021 00:20:51 -0800 Subject: Accounting for completely empty fields that only contain whitespaces. --- bot/exts/info/pypi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 7a5d7f4b7..990a5c905 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -37,7 +37,8 @@ class PyPi(Cog): embed.description = f"[{info['name']} v{info['version']}]({info['package_url']})\n" for field in FIELDS: - if field_value := info[field]: + # Field could be completely empty, in some cases can be a string with whitespaces. + if field_value := info[field].strip(): embed.add_field( name=field.replace("_", " ").title(), value=field_value, -- cgit v1.2.3 From 889de9b678a044331f02eef647c7d1c963f37edd Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 12 Feb 2021 00:49:27 -0800 Subject: Finalized logic to account for null cases. --- bot/exts/info/pypi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 990a5c905..4ad72b673 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -37,11 +37,11 @@ class PyPi(Cog): embed.description = f"[{info['name']} v{info['version']}]({info['package_url']})\n" for field in FIELDS: - # Field could be completely empty, in some cases can be a string with whitespaces. - if field_value := info[field].strip(): + # Field could be completely empty, in some cases can be a string with whitespaces, or None. + if info[field] and not info[field].isspace(): embed.add_field( name=field.replace("_", " ").title(), - value=field_value, + value=info[field], inline=False, ) -- cgit v1.2.3 From bcab6614bba3ca71edeb134089846570e0e47547 Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 12 Feb 2021 01:22:12 -0800 Subject: Moved hyperlink to title. --- bot/exts/info/pypi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 4ad72b673..c7ec22fc6 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -32,9 +32,9 @@ class PyPi(Cog): response_json = await response.json() info = response_json["info"] - embed.title = "Python Package Index" + embed.title = f"{info['name']} v{info['version']}" + embed.url = info['package_url'] embed.colour = Colours.soft_green - embed.description = f"[{info['name']} v{info['version']}]({info['package_url']})\n" for field in FIELDS: # Field could be completely empty, in some cases can be a string with whitespaces, or None. -- cgit v1.2.3 From 94ad4dd207226d7d1a2b080ffe47352e7c2b9e73 Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Fri, 12 Feb 2021 02:09:17 -0800 Subject: Made docstring more specific. Co-authored-by: Shivansh-007 <69356296+Shivansh-007@users.noreply.github.com> --- bot/exts/info/pypi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index c7ec22fc6..79931c665 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -21,7 +21,7 @@ class PyPi(Cog): @command(name="pypi", aliases=("package", "pack")) async def get_package_info(self, ctx: Context, package: str) -> None: - """Getting information about a specific package.""" + """Provide information about a specific package from PyPI.""" embed = Embed(title=choice(NEGATIVE_REPLIES), colour=Colours.soft_red) async with self.bot.http_session.get(URL.format(package=package)) as response: -- cgit v1.2.3 From 9ce9ab617ba0fdacb1922e2ed2007ed05e53c526 Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 12 Feb 2021 13:10:25 -0800 Subject: Added colours yellow, blue, and white. --- bot/constants.py | 9 ++++++--- config-default.yml | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 91e41e334..8a93ff9cf 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -246,13 +246,16 @@ class Colours(metaclass=YAMLGetter): section = "style" subsection = "colours" + blue: int bright_green: int - soft_green: int - soft_orange: int - soft_red: int orange: int pink: int purple: int + soft_green: int + soft_orange: int + soft_red: int + white: int + yellow: int class DuckPond(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index d7415c821..25bbcc3c5 100644 --- a/config-default.yml +++ b/config-default.yml @@ -24,13 +24,16 @@ bot: style: colours: + blue: 0x3775a8 bright_green: 0x01d277 - soft_green: 0x68c290 - soft_orange: 0xf9cb54 - soft_red: 0xcd6d6d orange: 0xe67e22 pink: 0xcf84e0 purple: 0xb734eb + soft_green: 0x68c290 + soft_orange: 0xf9cb54 + soft_red: 0xcd6d6d + white: 0xfffffe + yellow: 0xffd241 emojis: badge_bug_hunter: "<:bug_hunter_lvl1:743882896372269137>" -- cgit v1.2.3 From aa0b60534d1b8cef2e34bbaf50709553c71a14ff Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 12 Feb 2021 13:12:24 -0800 Subject: Rotating colours in embed, title now links to package. --- bot/exts/info/pypi.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index c7ec22fc6..c7d4d321c 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -1,5 +1,6 @@ +import itertools import logging -from random import choice +import random from discord import Embed from discord.ext.commands import Cog, Context, command @@ -8,7 +9,9 @@ from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES URL = "https://pypi.org/pypi/{package}/json" -FIELDS = ["author", "requires_python", "summary", "license"] +FIELDS = ("author", "requires_python", "summary", "license") +PYPI_ICON = "https://cdn.discordapp.com/emojis/766274397257334814.png" +PYPI_COLOURS = itertools.cycle((Colours.yellow, Colours.blue, Colours.white)) log = logging.getLogger(__name__) @@ -21,8 +24,12 @@ class PyPi(Cog): @command(name="pypi", aliases=("package", "pack")) async def get_package_info(self, ctx: Context, package: str) -> None: - """Getting information about a specific package.""" - embed = Embed(title=choice(NEGATIVE_REPLIES), colour=Colours.soft_red) + """Provide information about a specific package from PyPI.""" + embed = Embed( + title=random.choice(NEGATIVE_REPLIES), + colour=Colours.soft_red + ) + embed.set_thumbnail(url=PYPI_ICON) async with self.bot.http_session.get(URL.format(package=package)) as response: if response.status == 404: @@ -34,7 +41,7 @@ class PyPi(Cog): embed.title = f"{info['name']} v{info['version']}" embed.url = info['package_url'] - embed.colour = Colours.soft_green + embed.colour = next(PYPI_COLOURS) for field in FIELDS: # Field could be completely empty, in some cases can be a string with whitespaces, or None. -- cgit v1.2.3 From 059940b5ae3cc2921303579ebf161835fe09076d Mon Sep 17 00:00:00 2001 From: xithrius Date: Fri, 12 Feb 2021 15:47:06 -0800 Subject: Taking only the first line of multiline fields. --- bot/exts/info/pypi.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index c7d4d321c..cf45b068f 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -44,11 +44,16 @@ class PyPi(Cog): embed.colour = next(PYPI_COLOURS) for field in FIELDS: + field_data = info[field] + # Field could be completely empty, in some cases can be a string with whitespaces, or None. - if info[field] and not info[field].isspace(): + if field_data and not field_data.isspace(): + if '\n' in field_data and field == "license": + field_data = field_data.split('\n')[0] + embed.add_field( name=field.replace("_", " ").title(), - value=info[field], + value=field_data, inline=False, ) -- cgit v1.2.3 From 578bd933ca4b954131f25646e69748cc3d748d0b Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 06:39:16 +0200 Subject: Removed enabling and switched to redis Removing self.enable and the defon & defoff commands. Defcon will now just be always 'on' and we can set the days threshold to 0 to turn it off. Switched from postgres to redis - if the data gets lost we should just reconfigure defcon again, it should not depend on the site. --- bot/constants.py | 12 +-- bot/exts/moderation/defcon.py | 173 +++++++++++++----------------------------- config-default.yml | 12 +-- 3 files changed, 65 insertions(+), 132 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 95e22513f..cbab751d0 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -277,9 +277,9 @@ class Emojis(metaclass=YAMLGetter): badge_staff: str badge_verified_bot_developer: str - defcon_disabled: str # noqa: E704 - defcon_enabled: str # noqa: E704 - defcon_updated: str # noqa: E704 + defcon_shutdown: str # noqa: E704 + defcon_unshutdown: str # noqa: E704 + defcon_update: str # noqa: E704 failmail: str @@ -316,9 +316,9 @@ class Icons(metaclass=YAMLGetter): crown_red: str defcon_denied: str # noqa: E704 - defcon_disabled: str # noqa: E704 - defcon_enabled: str # noqa: E704 - defcon_updated: str # noqa: E704 + defcon_shutdown: str # noqa: E704 + defcon_unshutdown: str # noqa: E704 + defcon_update: str # noqa: E704 filtering: str diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index e0baab099..8e6ab1fd5 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from enum import Enum from gettext import ngettext +from async_rediscache import RedisCache from discord import Colour, Embed, Member from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role @@ -36,67 +37,59 @@ class Action(Enum): ActionInfo = namedtuple('LogInfoDetails', ['icon', 'color', 'template']) - ENABLED = ActionInfo(Icons.defcon_enabled, Colours.soft_green, "**Days:** {days}\n\n") - DISABLED = ActionInfo(Icons.defcon_disabled, Colours.soft_red, "") - UPDATED = ActionInfo(Icons.defcon_updated, Colour.blurple(), "**Days:** {days}\n\n") + SERVER_OPEN = ActionInfo(Icons.defcon_unshutdown, Colours.soft_green, "") + SERVER_SHUTDOWN = ActionInfo(Icons.defcon_shutdown, Colours.soft_red, "") + DURATION_UPDATE = ActionInfo(Icons.defcon_update, Colour.blurple(), "**Days:** {days}\n\n") class Defcon(Cog): """Time-sensitive server defense mechanisms.""" + redis_cache = RedisCache() + def __init__(self, bot: Bot): self.bot = bot self.channel = None self.days = timedelta(days=0) - self.enabled = False + self.expiry = None - self.bot.loop.create_task(self.sync_settings()) + self.bot.loop.create_task(self._sync_settings()) @property def mod_log(self) -> ModLog: """Get currently loaded ModLog cog instance.""" return self.bot.get_cog("ModLog") - async def sync_settings(self) -> None: + @redis_cache.atomic_transaction + async def _sync_settings(self) -> None: """On cog load, try to synchronize DEFCON settings to the API.""" log.trace("Waiting for the guild to become available before syncing.") await self.bot.wait_until_guild_available() + self.channel = await self.bot.fetch_channel(Channels.defcon) log.trace("Syncing settings.") - self.channel = await self.bot.fetch_channel(Channels.defcon) try: - response = await self.bot.api_client.get('bot/bot-settings/defcon') - data = response['data'] - - except Exception: # Yikes! + settings = await self.redis_cache.to_dict() + self.days = timedelta(days=settings["days"]) + except Exception: log.exception("Unable to get DEFCON settings!") - await self.bot.get_channel(Channels.dev_log).send( - f"<@&{Roles.admins}> **WARNING**: Unable to get DEFCON settings!" - ) + await self.channel.send(f"<@&{Roles.moderators}> **WARNING**: Unable to get DEFCON settings!") else: - if data["enabled"]: - self.enabled = True - self.days = timedelta(days=data["days"]) - log.info(f"DEFCON enabled: {self.days.days} days") - - else: - self.enabled = False - self.days = timedelta(days=0) - log.info("DEFCON disabled") + self._update_notifier() + log.info(f"DEFCON synchronized: {self.days.days} days") - self.update_notifier() - await self.update_channel_topic() + await self._update_channel_topic() @Cog.listener() async def on_member_join(self, member: Member) -> None: - """If DEFCON is enabled, check newly joining users to see if they meet the account age threshold.""" - if self.enabled and self.days.days > 0: + """Check newly joining users to see if they meet the account age threshold.""" + if self.days.days > 0: now = datetime.utcnow() if now - member.created_at < self.days: - log.info(f"Rejecting user {member}: Account is too new and DEFCON is enabled") + log.info(f"Rejecting user {member}: Account is too new") message_sent = False @@ -104,7 +97,7 @@ class Defcon(Cog): await member.send(REJECTION_MESSAGE.format(user=member.mention)) message_sent = True - except Exception: + except Exception: # TODO log.exception(f"Unable to send rejection message to user: {member}") await member.kick(reason="DEFCON active, user is too new") @@ -128,118 +121,64 @@ class Defcon(Cog): """Check the DEFCON status or run a subcommand.""" await ctx.send_help(ctx.command) + @redis_cache.atomic_transaction async def _defcon_action(self, ctx: Context, days: int, action: Action) -> None: """Providing a structured way to do an defcon action.""" - try: - response = await self.bot.api_client.get('bot/bot-settings/defcon') - data = response['data'] - - if "enable_date" in data and action is Action.DISABLED: - enabled = datetime.fromisoformat(data["enable_date"]) - - delta = datetime.now() - enabled - - self.bot.stats.timing("defcon.enabled", delta) - except Exception: - pass - self.days = timedelta(days=days) - self.enabled = action != Action.DISABLED - error = None - try: - await self.bot.api_client.put( - 'bot/bot-settings/defcon', - json={ - 'name': 'defcon', - 'data': { - # TODO: retrieve old days count - 'days': self.days.days, - 'enabled': self.enabled, - 'enable_date': datetime.now().isoformat() - } - } - ) - - except Exception as err: - log.exception("Unable to update DEFCON settings.") - error = err - finally: - self.update_notifier() - - await ctx.send(self.build_defcon_msg(action, error)) - await self.send_defcon_log(action, ctx.author, error) - await self.update_channel_topic() - - self.bot.stats.gauge("defcon.threshold", days) + await self.redis_cache.update( + { + 'days': self.days.days, + } + ) + self._update_notifier() - @defcon_group.command(name='enable', aliases=('on', 'e'), root_aliases=("defon",)) - @has_any_role(*MODERATION_ROLES) - async def enable_command(self, ctx: Context) -> None: - """ - Enable DEFCON mode. Useful in a pinch, but be sure you know what you're doing! + await ctx.send(self._build_defcon_msg(action)) + await self._send_defcon_log(action, ctx.author) + await self._update_channel_topic() - Currently, this just adds an account age requirement. Use !defcon days to set how old an account must be, - in days. - """ - await self._defcon_action(ctx, days=self.days, action=Action.ENABLED) + self.bot.stats.gauge("defcon.threshold", days) - @defcon_group.command(name='disable', aliases=('off', 'd'), root_aliases=("defoff",)) + @defcon_group.command(aliases=('s',)) @has_any_role(*MODERATION_ROLES) - async def disable_command(self, ctx: Context) -> None: - """Disable DEFCON mode. Useful in a pinch, but be sure you know what you're doing!""" - await self._defcon_action(ctx, days=0, action=Action.DISABLED) - - @defcon_group.command(name='status', aliases=('s',)) - @has_any_role(*MODERATION_ROLES) - async def status_command(self, ctx: Context) -> None: + async def status(self, ctx: Context) -> None: """Check the current status of DEFCON mode.""" embed = Embed( colour=Colour.blurple(), title="DEFCON Status", - description=f"**Enabled:** {self.enabled}\n" - f"**Days:** {self.days.days}" + description=f"**Days:** {self.days.days}" ) await ctx.send(embed=embed) - @defcon_group.command(name='days') + @defcon_group.command(aliases=('d',)) @has_any_role(*MODERATION_ROLES) - async def days_command(self, ctx: Context, days: int) -> None: - """Set how old an account must be to join the server, in days, with DEFCON mode enabled.""" - await self._defcon_action(ctx, days=days, action=Action.UPDATED) + async def days(self, ctx: Context, days: int) -> None: + """Set how old an account must be to join the server, in days.""" + await self._defcon_action(ctx, days=days, action=Action.DURATION_UPDATE) - async def update_channel_topic(self) -> None: + async def _update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" - if self.enabled: - day_str = "days" if self.days.days > 1 else "day" - new_topic = f"{BASE_CHANNEL_TOPIC}\n(Status: Enabled, Threshold: {self.days.days} {day_str})" - else: - new_topic = f"{BASE_CHANNEL_TOPIC}\n(Status: Disabled)" + day_str = "days" if self.days.days > 1 else "day" + new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {self.days.days} {day_str})" self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) await self.channel.edit(topic=new_topic) - def build_defcon_msg(self, action: Action, e: Exception = None) -> str: + def _build_defcon_msg(self, action: Action) -> str: """Build in-channel response string for DEFCON action.""" - if action is Action.ENABLED: - msg = f"{Emojis.defcon_enabled} DEFCON enabled.\n\n" - elif action is Action.DISABLED: - msg = f"{Emojis.defcon_disabled} DEFCON disabled.\n\n" - elif action is Action.UPDATED: + if action is Action.SERVER_OPEN: + msg = f"{Emojis.defcon_enabled} Server reopened.\n\n" + elif action is Action.SERVER_SHUTDOWN: + msg = f"{Emojis.defcon_disabled} Server shut down.\n\n" + elif action is Action.DURATION_UPDATE: msg = ( - f"{Emojis.defcon_updated} DEFCON days updated; accounts must be {self.days.days} " - f"day{'s' if self.days.days > 1 else ''} old to join the server.\n\n" - ) - - if e: - msg += ( - "**There was a problem updating the site** - This setting may be reverted when the bot restarts.\n\n" - f"```py\n{e}\n```" + f"{Emojis.defcon_update} DEFCON days updated; accounts must be {self.days.days} " + f"day{ngettext('', 's', self.days.days)} old to join the server.\n\n" ) return msg - async def send_defcon_log(self, action: Action, actor: Member, e: Exception = None) -> None: + async def _send_defcon_log(self, action: Action, actor: Member) -> None: """Send log message for DEFCON action.""" info = action.value log_msg: str = ( @@ -248,15 +187,9 @@ class Defcon(Cog): ) status_msg = f"DEFCON {action.name.lower()}" - if e: - log_msg += ( - "**There was a problem updating the site** - This setting may be reverted when the bot restarts.\n\n" - f"```py\n{e}\n```" - ) - await self.mod_log.send_log_message(info.icon, info.color, status_msg, log_msg) - def update_notifier(self) -> None: + def _update_notifier(self) -> None: """Start or stop the notifier according to the DEFCON status.""" if self.days.days != 0 and not self.defcon_notifier.is_running(): log.info("DEFCON notifier started.") diff --git a/config-default.yml b/config-default.yml index d3b267159..a37743c15 100644 --- a/config-default.yml +++ b/config-default.yml @@ -44,9 +44,9 @@ style: badge_staff: "<:discord_staff:743882896498098226>" badge_verified_bot_developer: "<:verified_bot_dev:743882897299210310>" - defcon_disabled: "<:defcondisabled:470326273952972810>" - defcon_enabled: "<:defconenabled:470326274213150730>" - defcon_updated: "<:defconsettingsupdated:470326274082996224>" + defcon_shutdown: "<:defcondisabled:470326273952972810>" + defcon_unshutdown: "<:defconenabled:470326274213150730>" + defcon_update: "<:defconsettingsupdated:470326274082996224>" failmail: "<:failmail:633660039931887616>" @@ -80,9 +80,9 @@ style: crown_red: "https://cdn.discordapp.com/emojis/469964154879344640.png" defcon_denied: "https://cdn.discordapp.com/emojis/472475292078964738.png" - defcon_disabled: "https://cdn.discordapp.com/emojis/470326273952972810.png" - defcon_enabled: "https://cdn.discordapp.com/emojis/470326274213150730.png" - defcon_updated: "https://cdn.discordapp.com/emojis/472472638342561793.png" + defcon_shutdown: "https://cdn.discordapp.com/emojis/470326273952972810.png" + defcon_unshutdown: "https://cdn.discordapp.com/emojis/470326274213150730.png" + defcon_update: "https://cdn.discordapp.com/emojis/472472638342561793.png" filtering: "https://cdn.discordapp.com/emojis/472472638594482195.png" -- cgit v1.2.3 From 6435646ef04e72528c9cba4ae04f29d662877573 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 06:53:00 +0200 Subject: Reordered methods --- bot/exts/moderation/defcon.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 8e6ab1fd5..355843bc8 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -121,24 +121,6 @@ class Defcon(Cog): """Check the DEFCON status or run a subcommand.""" await ctx.send_help(ctx.command) - @redis_cache.atomic_transaction - async def _defcon_action(self, ctx: Context, days: int, action: Action) -> None: - """Providing a structured way to do an defcon action.""" - self.days = timedelta(days=days) - - await self.redis_cache.update( - { - 'days': self.days.days, - } - ) - self._update_notifier() - - await ctx.send(self._build_defcon_msg(action)) - await self._send_defcon_log(action, ctx.author) - await self._update_channel_topic() - - self.bot.stats.gauge("defcon.threshold", days) - @defcon_group.command(aliases=('s',)) @has_any_role(*MODERATION_ROLES) async def status(self, ctx: Context) -> None: @@ -164,6 +146,24 @@ class Defcon(Cog): self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) await self.channel.edit(topic=new_topic) + @redis_cache.atomic_transaction + async def _defcon_action(self, ctx: Context, days: int, action: Action) -> None: + """Providing a structured way to do an defcon action.""" + self.days = timedelta(days=days) + + await self.redis_cache.update( + { + 'days': self.days.days, + } + ) + self._update_notifier() + + await ctx.send(self._build_defcon_msg(action)) + await self._send_defcon_log(action, ctx.author) + await self._update_channel_topic() + + self.bot.stats.gauge("defcon.threshold", days) + def _build_defcon_msg(self, action: Action) -> str: """Build in-channel response string for DEFCON action.""" if action is Action.SERVER_OPEN: -- cgit v1.2.3 From a2eaa58ff5bb5876e53c31e5efb979aff71c4745 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 07:47:31 +0200 Subject: Added server shutdown and reopen commands --- bot/exts/moderation/defcon.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 355843bc8..4aed24559 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -138,6 +138,28 @@ class Defcon(Cog): """Set how old an account must be to join the server, in days.""" await self._defcon_action(ctx, days=days, action=Action.DURATION_UPDATE) + @defcon_group.command() + @has_any_role(*MODERATION_ROLES) + async def shutdown(self, ctx: Context) -> None: + """Shut down the server by setting send permissions of everyone to False.""" + role = ctx.guild.default_role + permissions = role.permissions + + permissions.update(send_messages=False, add_reactions=False) + await role.edit(reason="DEFCON shutdown", permissions=permissions) + await ctx.send(self._build_defcon_msg(Action.SERVER_SHUTDOWN)) + + @defcon_group.command() + @has_any_role(*MODERATION_ROLES) + async def unshutdown(self, ctx: Context) -> None: + """Open up the server again by setting send permissions of everyone to None.""" + role = ctx.guild.default_role + permissions = role.permissions + + permissions.update(send_messages=True, add_reactions=True) + await role.edit(reason="DEFCON unshutdown", permissions=permissions) + await ctx.send(self._build_defcon_msg(Action.SERVER_OPEN)) + async def _update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" day_str = "days" if self.days.days > 1 else "day" @@ -167,9 +189,9 @@ class Defcon(Cog): def _build_defcon_msg(self, action: Action) -> str: """Build in-channel response string for DEFCON action.""" if action is Action.SERVER_OPEN: - msg = f"{Emojis.defcon_enabled} Server reopened.\n\n" + msg = f"{Emojis.defcon_unshutdown} Server reopened.\n\n" elif action is Action.SERVER_SHUTDOWN: - msg = f"{Emojis.defcon_disabled} Server shut down.\n\n" + msg = f"{Emojis.defcon_shutdown} Server shut down.\n\n" elif action is Action.DURATION_UPDATE: msg = ( f"{Emojis.defcon_update} DEFCON days updated; accounts must be {self.days.days} " -- cgit v1.2.3 From 72f258c107c3b577298c5e131897cb93790c67c4 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 08:08:23 +0200 Subject: Removed _build_defcon_message method --- bot/exts/moderation/defcon.py | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 4aed24559..b04752abd 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -35,11 +35,11 @@ BASE_CHANNEL_TOPIC = "Python Discord Defense Mechanism" class Action(Enum): """Defcon Action.""" - ActionInfo = namedtuple('LogInfoDetails', ['icon', 'color', 'template']) + ActionInfo = namedtuple('LogInfoDetails', ['icon', 'emoji', 'color', 'template']) - SERVER_OPEN = ActionInfo(Icons.defcon_unshutdown, Colours.soft_green, "") - SERVER_SHUTDOWN = ActionInfo(Icons.defcon_shutdown, Colours.soft_red, "") - DURATION_UPDATE = ActionInfo(Icons.defcon_update, Colour.blurple(), "**Days:** {days}\n\n") + SERVER_OPEN = ActionInfo(Icons.defcon_unshutdown, Emojis.defcon_unshutdown, Colours.soft_green, "") + SERVER_SHUTDOWN = ActionInfo(Icons.defcon_shutdown, Emojis.defcon_shutdown, Colours.soft_red, "") + DURATION_UPDATE = ActionInfo(Icons.defcon_update, Emojis.defcon_update, Colour.blurple(), "**Days:** {days}\n\n") class Defcon(Cog): @@ -136,7 +136,7 @@ class Defcon(Cog): @has_any_role(*MODERATION_ROLES) async def days(self, ctx: Context, days: int) -> None: """Set how old an account must be to join the server, in days.""" - await self._defcon_action(ctx, days=days, action=Action.DURATION_UPDATE) + await self._defcon_action(ctx, days=days) @defcon_group.command() @has_any_role(*MODERATION_ROLES) @@ -147,7 +147,7 @@ class Defcon(Cog): permissions.update(send_messages=False, add_reactions=False) await role.edit(reason="DEFCON shutdown", permissions=permissions) - await ctx.send(self._build_defcon_msg(Action.SERVER_SHUTDOWN)) + await ctx.send(f"{Action.SERVER_SHUTDOWN.value.emoji} Server shut down.") @defcon_group.command() @has_any_role(*MODERATION_ROLES) @@ -158,7 +158,7 @@ class Defcon(Cog): permissions.update(send_messages=True, add_reactions=True) await role.edit(reason="DEFCON unshutdown", permissions=permissions) - await ctx.send(self._build_defcon_msg(Action.SERVER_OPEN)) + await ctx.send(f"{Action.SERVER_OPEN.value.emoji} Server reopened.") async def _update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" @@ -169,8 +169,8 @@ class Defcon(Cog): await self.channel.edit(topic=new_topic) @redis_cache.atomic_transaction - async def _defcon_action(self, ctx: Context, days: int, action: Action) -> None: - """Providing a structured way to do an defcon action.""" + async def _defcon_action(self, ctx: Context, days: int) -> None: + """Providing a structured way to do a defcon action.""" self.days = timedelta(days=days) await self.redis_cache.update( @@ -180,26 +180,17 @@ class Defcon(Cog): ) self._update_notifier() - await ctx.send(self._build_defcon_msg(action)) + action = Action.DURATION_UPDATE + + await ctx.send( + f"{action.value.emoji} DEFCON days updated; accounts must be {self.days.days} " + f"day{ngettext('', 's', self.days.days)} old to join the server." + ) await self._send_defcon_log(action, ctx.author) await self._update_channel_topic() self.bot.stats.gauge("defcon.threshold", days) - def _build_defcon_msg(self, action: Action) -> str: - """Build in-channel response string for DEFCON action.""" - if action is Action.SERVER_OPEN: - msg = f"{Emojis.defcon_unshutdown} Server reopened.\n\n" - elif action is Action.SERVER_SHUTDOWN: - msg = f"{Emojis.defcon_shutdown} Server shut down.\n\n" - elif action is Action.DURATION_UPDATE: - msg = ( - f"{Emojis.defcon_update} DEFCON days updated; accounts must be {self.days.days} " - f"day{ngettext('', 's', self.days.days)} old to join the server.\n\n" - ) - - return msg - async def _send_defcon_log(self, action: Action, actor: Member) -> None: """Send log message for DEFCON action.""" info = action.value -- cgit v1.2.3 From 8fcb4c6ee7718143c949aa41627064635b2b364b Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 13 Feb 2021 08:24:04 +0200 Subject: Move Git SHA defining at end of Dockerfile to re-enable caching Defining SHA at the beginning of build breaks caching, so this should be avoided. --- Dockerfile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5d0380b44..994b8ee49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,10 @@ 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 \ - GIT_SHA=$git_sha + PIPENV_NOSPIN=1 RUN apt-get -y update \ && apt-get install -y \ @@ -25,6 +21,12 @@ WORKDIR /bot COPY Pipfile* ./ RUN pipenv install --system --deploy +# Define Git SHA build argument +ARG git_sha="development" + +# Set Git SHA environment variable here to enable caching +ENV GIT_SHA=$git_sha + # Copy the source code in last to optimize rebuilding the image COPY . . -- cgit v1.2.3 From 2016dceff88642b92564e8f0c8ec98db0cbedf29 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 09:26:33 +0200 Subject: Added cog check to only allow mods in the defcon channel --- bot/exts/moderation/defcon.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index b04752abd..a5af1141f 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -116,13 +116,11 @@ class Defcon(Cog): ) @group(name='defcon', aliases=('dc',), invoke_without_command=True) - @has_any_role(*MODERATION_ROLES) async def defcon_group(self, ctx: Context) -> None: """Check the DEFCON status or run a subcommand.""" await ctx.send_help(ctx.command) @defcon_group.command(aliases=('s',)) - @has_any_role(*MODERATION_ROLES) async def status(self, ctx: Context) -> None: """Check the current status of DEFCON mode.""" embed = Embed( @@ -133,13 +131,11 @@ class Defcon(Cog): await ctx.send(embed=embed) @defcon_group.command(aliases=('d',)) - @has_any_role(*MODERATION_ROLES) async def days(self, ctx: Context, days: int) -> None: """Set how old an account must be to join the server, in days.""" await self._defcon_action(ctx, days=days) @defcon_group.command() - @has_any_role(*MODERATION_ROLES) async def shutdown(self, ctx: Context) -> None: """Shut down the server by setting send permissions of everyone to False.""" role = ctx.guild.default_role @@ -150,7 +146,6 @@ class Defcon(Cog): await ctx.send(f"{Action.SERVER_SHUTDOWN.value.emoji} Server shut down.") @defcon_group.command() - @has_any_role(*MODERATION_ROLES) async def unshutdown(self, ctx: Context) -> None: """Open up the server again by setting send permissions of everyone to None.""" role = ctx.guild.default_role @@ -217,6 +212,10 @@ class Defcon(Cog): """Routinely notify moderators that DEFCON is active.""" await self.channel.send(f"Defcon is on and is set to {self.days.days} day{ngettext('', 's', self.days.days)}.") + async def cog_check(self, ctx: Context) -> bool: + """Only allow moderators in the defcon channel to run commands in this cog.""" + return has_any_role(*MODERATION_ROLES).predicate(ctx) and ctx.channel == self.channel + def cog_unload(self) -> None: """Cancel the notifer task when the cog unloads.""" log.trace("Cog unload: canceling defcon notifier task.") -- cgit v1.2.3 From 88b5b32696c876ee0aa5299eb78bb0d775c5b800 Mon Sep 17 00:00:00 2001 From: xithrius Date: Sat, 13 Feb 2021 00:25:09 -0800 Subject: Escaping markdown in all fields that are created. --- bot/exts/info/pypi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index cf45b068f..73ec31870 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -7,6 +7,7 @@ from discord.ext.commands import Cog, Context, command from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES +from discord.utils import escape_markdown URL = "https://pypi.org/pypi/{package}/json" FIELDS = ("author", "requires_python", "summary", "license") @@ -53,7 +54,7 @@ class PyPi(Cog): embed.add_field( name=field.replace("_", " ").title(), - value=field_data, + value=escape_markdown(field_data), inline=False, ) -- cgit v1.2.3 From f14c391e3a1228953cda29be7993c9a5ec51ca6f Mon Sep 17 00:00:00 2001 From: xithrius Date: Sat, 13 Feb 2021 00:34:33 -0800 Subject: Made flake8 even happier. --- bot/exts/info/pypi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 73ec31870..3e326e8bb 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -4,10 +4,10 @@ import random from discord import Embed from discord.ext.commands import Cog, Context, command +from discord.utils import escape_markdown from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES -from discord.utils import escape_markdown URL = "https://pypi.org/pypi/{package}/json" FIELDS = ("author", "requires_python", "summary", "license") -- cgit v1.2.3 From d99f7d88f4718ae8042b22788c6ec85541219ae7 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 12:19:01 +0200 Subject: Defcon days is now defcon threshold with DurationDelta --- bot/converters.py | 17 ++-------- bot/exts/moderation/defcon.py | 72 +++++++++++++++++++++++++++---------------- bot/utils/time.py | 36 ++++++++++++++++++++++ 3 files changed, 84 insertions(+), 41 deletions(-) diff --git a/bot/converters.py b/bot/converters.py index d0a9731d6..483272de1 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -16,6 +16,7 @@ from discord.utils import DISCORD_EPOCH, snowflake_time from bot.api import ResponseCodeError from bot.constants import URLs from bot.utils.regex import INVITE_RE +from bot.utils.time import parse_duration_string log = logging.getLogger(__name__) @@ -301,16 +302,6 @@ class TagContentConverter(Converter): class DurationDelta(Converter): """Convert duration strings into dateutil.relativedelta.relativedelta objects.""" - duration_parser = re.compile( - r"((?P\d+?) ?(years|year|Y|y) ?)?" - r"((?P\d+?) ?(months|month|m) ?)?" - r"((?P\d+?) ?(weeks|week|W|w) ?)?" - r"((?P\d+?) ?(days|day|D|d) ?)?" - r"((?P\d+?) ?(hours|hour|H|h) ?)?" - r"((?P\d+?) ?(minutes|minute|M) ?)?" - r"((?P\d+?) ?(seconds|second|S|s))?" - ) - async def convert(self, ctx: Context, duration: str) -> relativedelta: """ Converts a `duration` string to a relativedelta object. @@ -326,13 +317,9 @@ class DurationDelta(Converter): The units need to be provided in descending order of magnitude. """ - match = self.duration_parser.fullmatch(duration) - if not match: + if not (delta := parse_duration_string(duration)): raise BadArgument(f"`{duration}` is not a valid duration string.") - duration_dict = {unit: int(amount) for unit, amount in match.groupdict(default=0).items()} - delta = relativedelta(**duration_dict) - return delta diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index a5af1141f..82aaf5714 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -2,19 +2,22 @@ from __future__ import annotations import logging from collections import namedtuple -from datetime import datetime, timedelta +from datetime import datetime from enum import Enum -from gettext import ngettext +from typing import Union from async_rediscache import RedisCache +from dateutil.relativedelta import relativedelta from discord import Colour, Embed, Member from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles +from bot.converters import DurationDelta from bot.exts.moderation.modlog import ModLog from bot.utils.messages import format_user +from bot.utils.time import humanize_delta, parse_duration_string log = logging.getLogger(__name__) @@ -31,6 +34,8 @@ will be resolved soon. In the meantime, please feel free to peruse the resources BASE_CHANNEL_TOPIC = "Python Discord Defense Mechanism" +SECONDS_IN_DAY = 86400 + class Action(Enum): """Defcon Action.""" @@ -39,7 +44,9 @@ class Action(Enum): SERVER_OPEN = ActionInfo(Icons.defcon_unshutdown, Emojis.defcon_unshutdown, Colours.soft_green, "") SERVER_SHUTDOWN = ActionInfo(Icons.defcon_shutdown, Emojis.defcon_shutdown, Colours.soft_red, "") - DURATION_UPDATE = ActionInfo(Icons.defcon_update, Emojis.defcon_update, Colour.blurple(), "**Days:** {days}\n\n") + DURATION_UPDATE = ActionInfo( + Icons.defcon_update, Emojis.defcon_update, Colour.blurple(), "**Threshold:** {threshold}\n\n" + ) class Defcon(Cog): @@ -50,7 +57,7 @@ class Defcon(Cog): def __init__(self, bot: Bot): self.bot = bot self.channel = None - self.days = timedelta(days=0) + self.threshold = relativedelta(days=0) self.expiry = None self.bot.loop.create_task(self._sync_settings()) @@ -71,24 +78,24 @@ class Defcon(Cog): try: settings = await self.redis_cache.to_dict() - self.days = timedelta(days=settings["days"]) + self.threshold = parse_duration_string(settings["threshold"]) except Exception: log.exception("Unable to get DEFCON settings!") await self.channel.send(f"<@&{Roles.moderators}> **WARNING**: Unable to get DEFCON settings!") else: self._update_notifier() - log.info(f"DEFCON synchronized: {self.days.days} days") + log.info(f"DEFCON synchronized: {humanize_delta(self.threshold)}") await self._update_channel_topic() @Cog.listener() async def on_member_join(self, member: Member) -> None: """Check newly joining users to see if they meet the account age threshold.""" - if self.days.days > 0: + if self.threshold > relativedelta(days=0): now = datetime.utcnow() - if now - member.created_at < self.days: + if now - member.created_at < self.threshold: log.info(f"Rejecting user {member}: Account is too new") message_sent = False @@ -125,15 +132,17 @@ class Defcon(Cog): """Check the current status of DEFCON mode.""" embed = Embed( colour=Colour.blurple(), title="DEFCON Status", - description=f"**Days:** {self.days.days}" + description=f"**Threshold:** {humanize_delta(self.threshold)}" ) await ctx.send(embed=embed) - @defcon_group.command(aliases=('d',)) - async def days(self, ctx: Context, days: int) -> None: - """Set how old an account must be to join the server, in days.""" - await self._defcon_action(ctx, days=days) + @defcon_group.command(aliases=('t',)) + async def threshold(self, ctx: Context, threshold: Union[DurationDelta, int]) -> None: + """Set how old an account must be to join the server.""" + if isinstance(threshold, int): + threshold = relativedelta(days=threshold) + await self._defcon_action(ctx, threshold=threshold) @defcon_group.command() async def shutdown(self, ctx: Context) -> None: @@ -157,20 +166,19 @@ class Defcon(Cog): async def _update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" - day_str = "days" if self.days.days > 1 else "day" - new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {self.days.days} {day_str})" + new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {humanize_delta(self.threshold)})" self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) await self.channel.edit(topic=new_topic) @redis_cache.atomic_transaction - async def _defcon_action(self, ctx: Context, days: int) -> None: + async def _defcon_action(self, ctx: Context, threshold: relativedelta) -> None: """Providing a structured way to do a defcon action.""" - self.days = timedelta(days=days) + self.threshold = threshold await self.redis_cache.update( { - 'days': self.days.days, + 'threshold': Defcon._stringify_relativedelta(self.threshold), } ) self._update_notifier() @@ -178,20 +186,32 @@ class Defcon(Cog): action = Action.DURATION_UPDATE await ctx.send( - f"{action.value.emoji} DEFCON days updated; accounts must be {self.days.days} " - f"day{ngettext('', 's', self.days.days)} old to join the server." + f"{action.value.emoji} DEFCON threshold updated; accounts must be " + f"{humanize_delta(self.threshold)} old to join the server." ) await self._send_defcon_log(action, ctx.author) await self._update_channel_topic() - self.bot.stats.gauge("defcon.threshold", days) + self._log_threshold_stat(threshold) + + @staticmethod + def _stringify_relativedelta(delta: relativedelta) -> str: + """Convert a relativedelta object to a duration string.""" + units = [("years", "y"), ("months", "m"), ("days", "d"), ("hours", "h"), ("minutes", "m"), ("seconds", "s")] + return "".join(f"{getattr(delta, unit)}{symbol}" for unit, symbol in units if getattr(delta, unit)) or "0s" + + def _log_threshold_stat(self, threshold: relativedelta) -> None: + """Adds the threshold to the bot stats in days.""" + utcnow = datetime.utcnow() + threshold_days = (utcnow + threshold - utcnow).total_seconds() / SECONDS_IN_DAY + self.bot.stats.gauge("defcon.threshold", threshold_days) async def _send_defcon_log(self, action: Action, actor: Member) -> None: """Send log message for DEFCON action.""" info = action.value log_msg: str = ( f"**Staffer:** {actor.mention} {actor} (`{actor.id}`)\n" - f"{info.template.format(days=self.days.days)}" + f"{info.template.format(threshold=humanize_delta(self.threshold))}" ) status_msg = f"DEFCON {action.name.lower()}" @@ -199,22 +219,22 @@ class Defcon(Cog): def _update_notifier(self) -> None: """Start or stop the notifier according to the DEFCON status.""" - if self.days.days != 0 and not self.defcon_notifier.is_running(): + if self.threshold != relativedelta(days=0) and not self.defcon_notifier.is_running(): log.info("DEFCON notifier started.") self.defcon_notifier.start() - elif self.days.days == 0 and self.defcon_notifier.is_running(): + elif self.threshold == relativedelta(days=0) and self.defcon_notifier.is_running(): log.info("DEFCON notifier stopped.") self.defcon_notifier.cancel() @tasks.loop(hours=1) async def defcon_notifier(self) -> None: """Routinely notify moderators that DEFCON is active.""" - await self.channel.send(f"Defcon is on and is set to {self.days.days} day{ngettext('', 's', self.days.days)}.") + await self.channel.send(f"Defcon is on and is set to {humanize_delta(self.threshold)}.") async def cog_check(self, ctx: Context) -> bool: """Only allow moderators in the defcon channel to run commands in this cog.""" - return has_any_role(*MODERATION_ROLES).predicate(ctx) and ctx.channel == self.channel + return (await has_any_role(*MODERATION_ROLES).predicate(ctx)) and ctx.channel == self.channel def cog_unload(self) -> None: """Cancel the notifer task when the cog unloads.""" diff --git a/bot/utils/time.py b/bot/utils/time.py index 47e49904b..5b197c350 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -1,5 +1,6 @@ import asyncio import datetime +import re from typing import Optional import dateutil.parser @@ -8,6 +9,16 @@ from dateutil.relativedelta import relativedelta RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" INFRACTION_FORMAT = "%Y-%m-%d %H:%M" +_duration_parser = re.compile( + r"((?P\d+?) ?(years|year|Y|y) ?)?" + r"((?P\d+?) ?(months|month|m) ?)?" + r"((?P\d+?) ?(weeks|week|W|w) ?)?" + r"((?P\d+?) ?(days|day|D|d) ?)?" + r"((?P\d+?) ?(hours|hour|H|h) ?)?" + r"((?P\d+?) ?(minutes|minute|M) ?)?" + r"((?P\d+?) ?(seconds|second|S|s))?" +) + def _stringify_time_unit(value: int, unit: str) -> str: """ @@ -74,6 +85,31 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: return humanized +def parse_duration_string(duration: str) -> Optional[relativedelta]: + """ + Converts a `duration` string to a relativedelta object. + + The function supports the following symbols for each unit of time: + - years: `Y`, `y`, `year`, `years` + - months: `m`, `month`, `months` + - weeks: `w`, `W`, `week`, `weeks` + - days: `d`, `D`, `day`, `days` + - hours: `H`, `h`, `hour`, `hours` + - minutes: `M`, `minute`, `minutes` + - seconds: `S`, `s`, `second`, `seconds` + The units need to be provided in descending order of magnitude. + If the string does represent a durationdelta object, it will return None. + """ + match = _duration_parser.fullmatch(duration) + if not match: + return None + + duration_dict = {unit: int(amount) for unit, amount in match.groupdict(default=0).items()} + delta = relativedelta(**duration_dict) + + return delta + + def time_since(past_datetime: datetime.datetime, precision: str = "seconds", max_units: int = 6) -> str: """ Takes a datetime and returns a human-readable string that describes how long ago that datetime was. -- cgit v1.2.3 From fdf12c6d2b2f3ab5ae335e2913a714cbeac2ff30 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 16:14:56 +0200 Subject: Add option to schedule threshold reset Added optional argument to defcon threshold to specify for how long it should be on. The notifier will now run only when there is no expiry date specified. --- bot/exts/moderation/defcon.py | 62 ++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 82aaf5714..8c21a7327 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -4,19 +4,20 @@ import logging from collections import namedtuple from datetime import datetime from enum import Enum -from typing import Union +from typing import Optional, Union from async_rediscache import RedisCache from dateutil.relativedelta import relativedelta -from discord import Colour, Embed, Member +from discord import Colour, Embed, Member, User from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles -from bot.converters import DurationDelta +from bot.converters import DurationDelta, Expiry from bot.exts.moderation.modlog import ModLog from bot.utils.messages import format_user +from bot.utils.scheduling import Scheduler from bot.utils.time import humanize_delta, parse_duration_string log = logging.getLogger(__name__) @@ -60,6 +61,8 @@ class Defcon(Cog): self.threshold = relativedelta(days=0) self.expiry = None + self.scheduler = Scheduler(self.__class__.__name__) + self.bot.loop.create_task(self._sync_settings()) @property @@ -79,11 +82,15 @@ class Defcon(Cog): try: settings = await self.redis_cache.to_dict() self.threshold = parse_duration_string(settings["threshold"]) + self.expiry = datetime.fromisoformat(settings["expiry"]) if settings["expiry"] else None except Exception: log.exception("Unable to get DEFCON settings!") await self.channel.send(f"<@&{Roles.moderators}> **WARNING**: Unable to get DEFCON settings!") else: + if self.expiry: + self.scheduler.schedule_at(self.expiry, 0, self._remove_threshold()) + self._update_notifier() log.info(f"DEFCON synchronized: {humanize_delta(self.threshold)}") @@ -95,7 +102,7 @@ class Defcon(Cog): if self.threshold > relativedelta(days=0): now = datetime.utcnow() - if now - member.created_at < self.threshold: + if now - member.created_at < self.threshold: # TODO log.info(f"Rejecting user {member}: Account is too new") message_sent = False @@ -104,7 +111,7 @@ class Defcon(Cog): await member.send(REJECTION_MESSAGE.format(user=member.mention)) message_sent = True - except Exception: # TODO + except Exception: log.exception(f"Unable to send rejection message to user: {member}") await member.kick(reason="DEFCON active, user is too new") @@ -132,17 +139,22 @@ class Defcon(Cog): """Check the current status of DEFCON mode.""" embed = Embed( colour=Colour.blurple(), title="DEFCON Status", - description=f"**Threshold:** {humanize_delta(self.threshold)}" + description=f""" + **Threshold:** {humanize_delta(self.threshold)} + **Expires in:** {humanize_delta(relativedelta(self.expiry, datetime.utcnow())) if self.expiry else "-"} + """ ) await ctx.send(embed=embed) @defcon_group.command(aliases=('t',)) - async def threshold(self, ctx: Context, threshold: Union[DurationDelta, int]) -> None: + async def threshold( + self, ctx: Context, threshold: Union[DurationDelta, int], expiry: Optional[Expiry] = None + ) -> None: """Set how old an account must be to join the server.""" if isinstance(threshold, int): threshold = relativedelta(days=threshold) - await self._defcon_action(ctx, threshold=threshold) + await self._defcon_action(ctx.author, threshold=threshold, expiry=expiry) @defcon_group.command() async def shutdown(self, ctx: Context) -> None: @@ -172,28 +184,45 @@ class Defcon(Cog): await self.channel.edit(topic=new_topic) @redis_cache.atomic_transaction - async def _defcon_action(self, ctx: Context, threshold: relativedelta) -> None: + async def _defcon_action(self, author: User, threshold: relativedelta, expiry: Optional[Expiry] = None) -> None: """Providing a structured way to do a defcon action.""" self.threshold = threshold + if threshold == relativedelta(days=0): # If the threshold is 0, we don't need to schedule anything + expiry = None + self.expiry = expiry + + # Either way, we cancel the old task. + self.scheduler.cancel_all() + if self.expiry is not None: + self.scheduler.schedule_at(expiry, 0, self._remove_threshold()) await self.redis_cache.update( { 'threshold': Defcon._stringify_relativedelta(self.threshold), + 'expiry': expiry.isoformat() if expiry else 0 } ) self._update_notifier() action = Action.DURATION_UPDATE - await ctx.send( + expiry_message = "" + if expiry: + expiry_message = f"for the next {humanize_delta(relativedelta(expiry, datetime.utcnow()))}" + + await self.channel.send( f"{action.value.emoji} DEFCON threshold updated; accounts must be " - f"{humanize_delta(self.threshold)} old to join the server." + f"{humanize_delta(self.threshold)} old to join the server {expiry_message}." ) - await self._send_defcon_log(action, ctx.author) + await self._send_defcon_log(action, author) await self._update_channel_topic() self._log_threshold_stat(threshold) + async def _remove_threshold(self) -> None: + """Resets the threshold back to 0.""" + await self._defcon_action(self.bot.user, relativedelta(days=0)) + @staticmethod def _stringify_relativedelta(delta: relativedelta) -> str: """Convert a relativedelta object to a duration string.""" @@ -206,7 +235,7 @@ class Defcon(Cog): threshold_days = (utcnow + threshold - utcnow).total_seconds() / SECONDS_IN_DAY self.bot.stats.gauge("defcon.threshold", threshold_days) - async def _send_defcon_log(self, action: Action, actor: Member) -> None: + async def _send_defcon_log(self, action: Action, actor: User) -> None: """Send log message for DEFCON action.""" info = action.value log_msg: str = ( @@ -219,11 +248,11 @@ class Defcon(Cog): def _update_notifier(self) -> None: """Start or stop the notifier according to the DEFCON status.""" - if self.threshold != relativedelta(days=0) and not self.defcon_notifier.is_running(): + if self.threshold != relativedelta(days=0) and self.expiry is None and not self.defcon_notifier.is_running(): log.info("DEFCON notifier started.") self.defcon_notifier.start() - elif self.threshold == relativedelta(days=0) and self.defcon_notifier.is_running(): + elif (self.threshold == relativedelta(days=0) or self.expiry is not None) and self.defcon_notifier.is_running(): log.info("DEFCON notifier stopped.") self.defcon_notifier.cancel() @@ -237,9 +266,10 @@ class Defcon(Cog): return (await has_any_role(*MODERATION_ROLES).predicate(ctx)) and ctx.channel == self.channel def cog_unload(self) -> None: - """Cancel the notifer task when the cog unloads.""" + """Cancel the notifer and threshold removal tasks when the cog unloads.""" log.trace("Cog unload: canceling defcon notifier task.") self.defcon_notifier.cancel() + self.scheduler.cancel_all() def setup(bot: Bot) -> None: -- cgit v1.2.3 From 2e4a069ac185d1d978070327e76faba4eefbd255 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 16:42:27 +0200 Subject: Fixed on_message --- bot/exts/moderation/defcon.py | 9 ++++----- bot/exts/moderation/slowmode.py | 4 +--- bot/utils/time.py | 6 ++++++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 8c21a7327..28a1a425f 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -18,7 +18,7 @@ from bot.converters import DurationDelta, Expiry from bot.exts.moderation.modlog import ModLog from bot.utils.messages import format_user from bot.utils.scheduling import Scheduler -from bot.utils.time import humanize_delta, parse_duration_string +from bot.utils.time import humanize_delta, parse_duration_string, relativedelta_to_timedelta log = logging.getLogger(__name__) @@ -99,10 +99,10 @@ class Defcon(Cog): @Cog.listener() async def on_member_join(self, member: Member) -> None: """Check newly joining users to see if they meet the account age threshold.""" - if self.threshold > relativedelta(days=0): + if self.threshold != relativedelta(days=0): now = datetime.utcnow() - if now - member.created_at < self.threshold: # TODO + if now - member.created_at < relativedelta_to_timedelta(self.threshold): log.info(f"Rejecting user {member}: Account is too new") message_sent = False @@ -231,8 +231,7 @@ class Defcon(Cog): def _log_threshold_stat(self, threshold: relativedelta) -> None: """Adds the threshold to the bot stats in days.""" - utcnow = datetime.utcnow() - threshold_days = (utcnow + threshold - utcnow).total_seconds() / SECONDS_IN_DAY + threshold_days = relativedelta_to_timedelta(threshold).total_seconds() / SECONDS_IN_DAY self.bot.stats.gauge("defcon.threshold", threshold_days) async def _send_defcon_log(self, action: Action, actor: User) -> None: diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index c449752e1..d8baff76a 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -1,5 +1,4 @@ import logging -from datetime import datetime from typing import Optional from dateutil.relativedelta import relativedelta @@ -54,8 +53,7 @@ class Slowmode(Cog): # Convert `dateutil.relativedelta.relativedelta` to `datetime.timedelta` # Must do this to get the delta in a particular unit of time - utcnow = datetime.utcnow() - slowmode_delay = (utcnow + delay - utcnow).total_seconds() + slowmode_delay = time.relativedelta_to_timedelta(delay).total_seconds() humanized_delay = time.humanize_delta(delay) diff --git a/bot/utils/time.py b/bot/utils/time.py index 5b197c350..a7b441327 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -110,6 +110,12 @@ def parse_duration_string(duration: str) -> Optional[relativedelta]: return delta +def relativedelta_to_timedelta(delta: relativedelta) -> datetime.timedelta: + """Converts a relativedelta object to a timedelta object.""" + utcnow = datetime.datetime.utcnow() + return utcnow + delta - utcnow + + def time_since(past_datetime: datetime.datetime, precision: str = "seconds", max_units: int = 6) -> str: """ Takes a datetime and returns a human-readable string that describes how long ago that datetime was. -- cgit v1.2.3 From 62bd358d9fe390ba4ac25e122e261a44276ad9e9 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 16:51:52 +0200 Subject: Status command displays verification level --- bot/exts/moderation/defcon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 28a1a425f..17b521b00 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -142,6 +142,7 @@ class Defcon(Cog): description=f""" **Threshold:** {humanize_delta(self.threshold)} **Expires in:** {humanize_delta(relativedelta(self.expiry, datetime.utcnow())) if self.expiry else "-"} + **Verification level:** {ctx.guild.verification_level.name} """ ) -- cgit v1.2.3 From 94f7a701034d53e82d04f2a08e5927f874c74f49 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 13 Feb 2021 17:34:57 +0200 Subject: Prevent channel description edit from locking commands Because some parts are defined as atomic transaction, we can't use them with channel description edits which are heavily rate limited. Description edits are now run in a separate task. --- bot/exts/moderation/defcon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 17b521b00..d1b99cb35 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import logging from collections import namedtuple from datetime import datetime @@ -182,7 +183,7 @@ class Defcon(Cog): new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {humanize_delta(self.threshold)})" self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) - await self.channel.edit(topic=new_topic) + asyncio.create_task(self.channel.edit(topic=new_topic)) @redis_cache.atomic_transaction async def _defcon_action(self, author: User, threshold: relativedelta, expiry: Optional[Expiry] = None) -> None: -- cgit v1.2.3 From abc3f1a30b60981f90acf2065507179010b39713 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 14 Feb 2021 04:34:02 +0200 Subject: _update_channel_topic not longer needs to be awaited It's important to note that it's appropriate for the sync and action methods to have a lock between them, because if an action is made before syncing is complete it gets screwed and starts throwing excpetion for every command. --- bot/exts/moderation/defcon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index d1b99cb35..3cc8960dd 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -95,7 +95,7 @@ class Defcon(Cog): self._update_notifier() log.info(f"DEFCON synchronized: {humanize_delta(self.threshold)}") - await self._update_channel_topic() + self._update_channel_topic() @Cog.listener() async def on_member_join(self, member: Member) -> None: @@ -178,7 +178,7 @@ class Defcon(Cog): await role.edit(reason="DEFCON unshutdown", permissions=permissions) await ctx.send(f"{Action.SERVER_OPEN.value.emoji} Server reopened.") - async def _update_channel_topic(self) -> None: + def _update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {humanize_delta(self.threshold)})" @@ -217,7 +217,7 @@ class Defcon(Cog): f"{humanize_delta(self.threshold)} old to join the server {expiry_message}." ) await self._send_defcon_log(action, author) - await self._update_channel_topic() + self._update_channel_topic() self._log_threshold_stat(threshold) -- cgit v1.2.3 From 5dabc88c31c212182c155ebe873fad7b04879682 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 14 Feb 2021 18:33:05 +0200 Subject: Removed cog check, shutdown restricted to admins --- bot/exts/moderation/defcon.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 3cc8960dd..daacf95b7 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -131,11 +131,13 @@ class Defcon(Cog): ) @group(name='defcon', aliases=('dc',), invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) async def defcon_group(self, ctx: Context) -> None: """Check the DEFCON status or run a subcommand.""" await ctx.send_help(ctx.command) @defcon_group.command(aliases=('s',)) + @has_any_role(*MODERATION_ROLES) async def status(self, ctx: Context) -> None: """Check the current status of DEFCON mode.""" embed = Embed( @@ -150,6 +152,7 @@ class Defcon(Cog): await ctx.send(embed=embed) @defcon_group.command(aliases=('t',)) + @has_any_role(*MODERATION_ROLES) async def threshold( self, ctx: Context, threshold: Union[DurationDelta, int], expiry: Optional[Expiry] = None ) -> None: @@ -159,6 +162,7 @@ class Defcon(Cog): await self._defcon_action(ctx.author, threshold=threshold, expiry=expiry) @defcon_group.command() + @has_any_role(Roles.admins) async def shutdown(self, ctx: Context) -> None: """Shut down the server by setting send permissions of everyone to False.""" role = ctx.guild.default_role @@ -169,6 +173,7 @@ class Defcon(Cog): await ctx.send(f"{Action.SERVER_SHUTDOWN.value.emoji} Server shut down.") @defcon_group.command() + @has_any_role(Roles.admins) async def unshutdown(self, ctx: Context) -> None: """Open up the server again by setting send permissions of everyone to None.""" role = ctx.guild.default_role @@ -262,10 +267,6 @@ class Defcon(Cog): """Routinely notify moderators that DEFCON is active.""" await self.channel.send(f"Defcon is on and is set to {humanize_delta(self.threshold)}.") - async def cog_check(self, ctx: Context) -> bool: - """Only allow moderators in the defcon channel to run commands in this cog.""" - return (await has_any_role(*MODERATION_ROLES).predicate(ctx)) and ctx.channel == self.channel - def cog_unload(self) -> None: """Cancel the notifer and threshold removal tasks when the cog unloads.""" log.trace("Cog unload: canceling defcon notifier task.") -- cgit v1.2.3 From 64598b37145cbd2a2ee25008ff6217ee7fe6de03 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 14 Feb 2021 18:37:05 +0200 Subject: Renamed _defcon_action to _update_threshold and updated docstring --- bot/exts/moderation/defcon.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index daacf95b7..cdc5ff1b0 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -159,7 +159,7 @@ class Defcon(Cog): """Set how old an account must be to join the server.""" if isinstance(threshold, int): threshold = relativedelta(days=threshold) - await self._defcon_action(ctx.author, threshold=threshold, expiry=expiry) + await self._update_threshold(ctx.author, threshold=threshold, expiry=expiry) @defcon_group.command() @has_any_role(Roles.admins) @@ -191,8 +191,8 @@ class Defcon(Cog): asyncio.create_task(self.channel.edit(topic=new_topic)) @redis_cache.atomic_transaction - async def _defcon_action(self, author: User, threshold: relativedelta, expiry: Optional[Expiry] = None) -> None: - """Providing a structured way to do a defcon action.""" + async def _update_threshold(self, author: User, threshold: relativedelta, expiry: Optional[Expiry] = None) -> None: + """Update the new threshold in the cog, cache, defcon channel, and logs, and additionally schedule expiry.""" self.threshold = threshold if threshold == relativedelta(days=0): # If the threshold is 0, we don't need to schedule anything expiry = None @@ -228,7 +228,7 @@ class Defcon(Cog): async def _remove_threshold(self) -> None: """Resets the threshold back to 0.""" - await self._defcon_action(self.bot.user, relativedelta(days=0)) + await self._update_threshold(self.bot.user, relativedelta(days=0)) @staticmethod def _stringify_relativedelta(delta: relativedelta) -> str: -- cgit v1.2.3 From 55f7e7085fe821b4af7e59e811148808a3a40738 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 14 Feb 2021 22:06:44 +0000 Subject: Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 994b8ee49..1a75e5669 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ RUN pipenv install --system --deploy # Define Git SHA build argument ARG git_sha="development" -# Set Git SHA environment variable here to enable caching +# Set Git SHA environment variable for Sentry ENV GIT_SHA=$git_sha # Copy the source code in last to optimize rebuilding the image -- cgit v1.2.3 From cd0e443656f0aca93691b19f3b80a6e2b7c0e228 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 17 Feb 2021 15:09:21 +0200 Subject: Add missing None reason returns of filters --- bot/exts/filters/filtering.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 6f1374cf4..0f403ca92 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -432,7 +432,7 @@ class Filtering(Cog): # Make sure it's not a URL if URL_RE.search(text): - return False + return False, None watchlist_patterns = self._get_filterlist_items('filter_token', allowed=False) for pattern in watchlist_patterns: @@ -445,7 +445,7 @@ class Filtering(Cog): async def _has_urls(self, text: str) -> Tuple[bool, Optional[str]]: """Returns True if the text contains one of the blacklisted URLs from the config file.""" if not URL_RE.search(text): - return False + return False, None text = text.lower() domain_blacklist = self._get_filterlist_items("domain_name", allowed=False) @@ -494,7 +494,7 @@ class Filtering(Cog): # Lack of a "guild" key in the JSON response indicates either an group DM invite, an # expired invite, or an invalid invite. The API does not currently differentiate # between invalid and expired invites - return True + return True, None guild_id = guild.get("id") guild_invite_whitelist = self._get_filterlist_items("guild_invite", allowed=True) -- cgit v1.2.3 From 480dd04bda402e86337bd9a3fb5b236f661c6055 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 17 Feb 2021 15:10:47 +0200 Subject: Rename filter trigger reason from entry command to reason --- bot/exts/filters/filtering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 0f403ca92..c0fa431f1 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -344,7 +344,7 @@ class Filtering(Cog): ping_everyone = Filter.ping_everyone and _filter.get("ping_everyone", True) eval_msg = "using !eval " if is_eval else "" - footer = f"Entry comment: {reason}" if reason else None + footer = f"Reason: {reason}" if reason else None message = ( f"The {filter_name} {_filter['type']} was triggered by {format_user(msg.author)} " f"{channel_str} {eval_msg}with [the following message]({msg.jump_url}):\n\n" @@ -388,7 +388,7 @@ class Filtering(Cog): if name == "filter_invites" and match is not True: additional_embeds = [] for _, data in match.items(): - reason = f"\n**Entry comment:**\n{data['reason']}" if data.get('reason') else "" + reason = f"\n**Reason:**\n{data['reason']}" if data.get('reason') else "" embed = discord.Embed(description=( f"**Members:**\n{data['members']}\n" f"**Active:**\n{data['active']}" -- cgit v1.2.3 From 90bba1ccabeeb34250f86f5bcca70d335dd0ac27 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 17 Feb 2021 15:20:59 +0200 Subject: Add comments about returning blacklist entry reasons to filters --- bot/exts/filters/filtering.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index c0fa431f1..616c40605 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -426,6 +426,7 @@ class Filtering(Cog): `word_watchlist`'s patterns are placed between word boundaries while `token_watchlist` is matched as-is. Spoilers are expanded, if any, and URLs are ignored. + Second return value is a reason written to database about blacklist entry (can be None). """ if SPOILER_RE.search(text): text = self._expand_spoilers(text) @@ -443,7 +444,11 @@ class Filtering(Cog): return False, None async def _has_urls(self, text: str) -> Tuple[bool, Optional[str]]: - """Returns True if the text contains one of the blacklisted URLs from the config file.""" + """ + Returns True if the text contains one of the blacklisted URLs from the config file. + + Second return value is a reason of URL blacklisting (can be None). + """ if not URL_RE.search(text): return False, None @@ -474,6 +479,8 @@ class Filtering(Cog): If none are detected, False is returned. Attempts to catch some of common ways to try to cheat the system. + + Return None as second value for compability with other filters. """ # Remove backslashes to prevent escape character aroundfuckery like # discord\.gg/gdudes-pony-farm @@ -535,7 +542,11 @@ class Filtering(Cog): @staticmethod async def _has_rich_embed(msg: Message) -> Tuple[Union[bool, List[discord.Embed]], None]: - """Determines if `msg` contains any rich embeds not auto-generated from a URL.""" + """ + Determines if `msg` contains any rich embeds not auto-generated from a URL. + + Return None as second value for compability with other filters. + """ if msg.embeds: for embed in msg.embeds: if embed.type == "rich": @@ -554,7 +565,11 @@ class Filtering(Cog): @staticmethod async def _has_everyone_ping(text: str) -> Tuple[bool, None]: - """Determines if `msg` contains an @everyone or @here ping outside of a codeblock.""" + """ + Determines if `msg` contains an @everyone or @here ping outside of a codeblock. + + Return None as second value for compability with other filters. + """ # First pass to avoid running re.sub on every message if not EVERYONE_PING_RE.search(text): return False, None -- cgit v1.2.3 From e4c77617d38e18fd00e6a58a4ce81d85181c1d90 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 17 Feb 2021 18:30:27 +0200 Subject: Changed server command to work with new defcon cog --- bot/exts/info/information.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 4499e4c25..577ec13f0 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -17,7 +17,7 @@ from bot.decorators import in_whitelist from bot.pagination import LinePaginator from bot.utils.channel import is_mod_channel, is_staff_channel from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check -from bot.utils.time import time_since +from bot.utils.time import humanize_delta, time_since log = logging.getLogger(__name__) @@ -52,7 +52,7 @@ class Information(Cog): ) return {role.name.title(): len(role.members) for role in roles} - def get_extended_server_info(self) -> str: + def get_extended_server_info(self, ctx: Context) -> str: """Return additional server info only visible in moderation channels.""" talentpool_info = "" if cog := self.bot.get_cog("Talentpool"): @@ -64,9 +64,9 @@ class Information(Cog): defcon_info = "" if cog := self.bot.get_cog("Defcon"): - defcon_status = "Enabled" if cog.enabled else "Disabled" - defcon_days = cog.days.days if cog.enabled else "-" - defcon_info = f"Defcon status: {defcon_status}\nDefcon days: {defcon_days}\n" + defcon_info = f"Defcon threshold: {humanize_delta(cog.threshold)}\n" + + verification = f"Verification level: {ctx.guild.verification_level.name}\n" python_general = self.bot.get_channel(constants.Channels.python_general) @@ -74,6 +74,7 @@ class Information(Cog): {talentpool_info}\ {bb_info}\ {defcon_info}\ + {verification}\ {python_general.mention} cooldown: {python_general.slowmode_delay}s """) @@ -198,7 +199,7 @@ class Information(Cog): # Additional info if ran in moderation channels if is_mod_channel(ctx.channel): - embed.add_field(name="Moderation:", value=self.get_extended_server_info()) + embed.add_field(name="Moderation:", value=self.get_extended_server_info(ctx)) await ctx.send(embed=embed) -- cgit v1.2.3 From cef88008c11f3b6ee12eced70a0265639abe20bd Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 17 Feb 2021 19:51:07 +0200 Subject: Gave more meaningful name and description to the cache --- bot/exts/moderation/defcon.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index cdc5ff1b0..44fb8dc8f 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -54,7 +54,10 @@ class Action(Enum): class Defcon(Cog): """Time-sensitive server defense mechanisms.""" - redis_cache = RedisCache() + # RedisCache[str, str] + # The cache's keys are "threshold" and "expiry". + # The caches' values are strings formatted as valid input to the DurationDelta converter. + defcon_settings = RedisCache() def __init__(self, bot: Bot): self.bot = bot @@ -71,7 +74,7 @@ class Defcon(Cog): """Get currently loaded ModLog cog instance.""" return self.bot.get_cog("ModLog") - @redis_cache.atomic_transaction + @defcon_settings.atomic_transaction async def _sync_settings(self) -> None: """On cog load, try to synchronize DEFCON settings to the API.""" log.trace("Waiting for the guild to become available before syncing.") @@ -81,7 +84,7 @@ class Defcon(Cog): log.trace("Syncing settings.") try: - settings = await self.redis_cache.to_dict() + settings = await self.defcon_settings.to_dict() self.threshold = parse_duration_string(settings["threshold"]) self.expiry = datetime.fromisoformat(settings["expiry"]) if settings["expiry"] else None except Exception: @@ -190,7 +193,7 @@ class Defcon(Cog): self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) asyncio.create_task(self.channel.edit(topic=new_topic)) - @redis_cache.atomic_transaction + @defcon_settings.atomic_transaction async def _update_threshold(self, author: User, threshold: relativedelta, expiry: Optional[Expiry] = None) -> None: """Update the new threshold in the cog, cache, defcon channel, and logs, and additionally schedule expiry.""" self.threshold = threshold @@ -203,7 +206,7 @@ class Defcon(Cog): if self.expiry is not None: self.scheduler.schedule_at(expiry, 0, self._remove_threshold()) - await self.redis_cache.update( + await self.defcon_settings.update( { 'threshold': Defcon._stringify_relativedelta(self.threshold), 'expiry': expiry.isoformat() if expiry else 0 -- cgit v1.2.3 From d36ea6430faefefc0b60c9f7ac87bc89aaf2b5b5 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 17 Feb 2021 20:03:30 +0200 Subject: Error loading settings will also ping devops role --- bot/constants.py | 1 + bot/exts/moderation/defcon.py | 4 +++- config-default.yml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/constants.py b/bot/constants.py index cbab751d0..65e8230c5 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -483,6 +483,7 @@ class Roles(metaclass=YAMLGetter): admins: int core_developers: int + devops: int helpers: int moderators: int owners: int diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 44fb8dc8f..66b551425 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -89,7 +89,9 @@ class Defcon(Cog): self.expiry = datetime.fromisoformat(settings["expiry"]) if settings["expiry"] else None except Exception: log.exception("Unable to get DEFCON settings!") - await self.channel.send(f"<@&{Roles.moderators}> **WARNING**: Unable to get DEFCON settings!") + await self.channel.send( + f"<@&{Roles.moderators}> <@&{Roles.devops}> **WARNING**: Unable to get DEFCON settings!" + ) else: if self.expiry: diff --git a/config-default.yml b/config-default.yml index a37743c15..59da23169 100644 --- a/config-default.yml +++ b/config-default.yml @@ -257,6 +257,7 @@ guild: # Staff admins: &ADMINS_ROLE 267628507062992896 core_developers: 587606783669829632 + devops: 409416496733880320 helpers: &HELPERS_ROLE 267630620367257601 moderators: &MODS_ROLE 267629731250176001 owners: &OWNERS_ROLE 267627879762755584 -- cgit v1.2.3 From be08ea954bce3e7e3f407cf72e78fe7e1aa9096e Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 17 Feb 2021 20:44:39 +0200 Subject: Threshold has false-y value when set to 0 --- bot/exts/moderation/defcon.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 66b551425..49f5a4ddd 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import asyncio import logging from collections import namedtuple @@ -56,7 +54,7 @@ class Defcon(Cog): # RedisCache[str, str] # The cache's keys are "threshold" and "expiry". - # The caches' values are strings formatted as valid input to the DurationDelta converter. + # The caches' values are strings formatted as valid input to the DurationDelta converter, or empty when off. defcon_settings = RedisCache() def __init__(self, bot: Bot): @@ -85,7 +83,7 @@ class Defcon(Cog): try: settings = await self.defcon_settings.to_dict() - self.threshold = parse_duration_string(settings["threshold"]) + self.threshold = parse_duration_string(settings["threshold"]) if settings["threshold"] else None self.expiry = datetime.fromisoformat(settings["expiry"]) if settings["expiry"] else None except Exception: log.exception("Unable to get DEFCON settings!") @@ -98,14 +96,14 @@ class Defcon(Cog): self.scheduler.schedule_at(self.expiry, 0, self._remove_threshold()) self._update_notifier() - log.info(f"DEFCON synchronized: {humanize_delta(self.threshold)}") + log.info(f"DEFCON synchronized: {humanize_delta(self.threshold) if self.threshold else '-'}") self._update_channel_topic() @Cog.listener() async def on_member_join(self, member: Member) -> None: """Check newly joining users to see if they meet the account age threshold.""" - if self.threshold != relativedelta(days=0): + if self.threshold: now = datetime.utcnow() if now - member.created_at < relativedelta_to_timedelta(self.threshold): @@ -148,7 +146,7 @@ class Defcon(Cog): embed = Embed( colour=Colour.blurple(), title="DEFCON Status", description=f""" - **Threshold:** {humanize_delta(self.threshold)} + **Threshold:** {humanize_delta(self.threshold) if self.threshold else "-"} **Expires in:** {humanize_delta(relativedelta(self.expiry, datetime.utcnow())) if self.expiry else "-"} **Verification level:** {ctx.guild.verification_level.name} """ @@ -190,7 +188,7 @@ class Defcon(Cog): def _update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" - new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {humanize_delta(self.threshold)})" + new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {humanize_delta(self.threshold) if self.threshold else '-'})" self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) asyncio.create_task(self.channel.edit(topic=new_topic)) @@ -210,7 +208,7 @@ class Defcon(Cog): await self.defcon_settings.update( { - 'threshold': Defcon._stringify_relativedelta(self.threshold), + 'threshold': Defcon._stringify_relativedelta(self.threshold) if self.threshold else "", 'expiry': expiry.isoformat() if expiry else 0 } ) @@ -220,11 +218,18 @@ class Defcon(Cog): expiry_message = "" if expiry: - expiry_message = f"for the next {humanize_delta(relativedelta(expiry, datetime.utcnow()))}" + expiry_message = f" for the next {humanize_delta(relativedelta(expiry, datetime.utcnow()), max_units=2)}" + + if self.threshold: + channel_message = ( + f"updated; accounts must be {humanize_delta(self.threshold)} " + f"old to join the server{expiry_message}" + ) + else: + channel_message = "removed" await self.channel.send( - f"{action.value.emoji} DEFCON threshold updated; accounts must be " - f"{humanize_delta(self.threshold)} old to join the server {expiry_message}." + f"{action.value.emoji} DEFCON threshold {channel_message}." ) await self._send_defcon_log(action, author) self._update_channel_topic() @@ -251,7 +256,7 @@ class Defcon(Cog): info = action.value log_msg: str = ( f"**Staffer:** {actor.mention} {actor} (`{actor.id}`)\n" - f"{info.template.format(threshold=humanize_delta(self.threshold))}" + f"{info.template.format(threshold=(humanize_delta(self.threshold) if self.threshold else '-'))}" ) status_msg = f"DEFCON {action.name.lower()}" @@ -259,11 +264,11 @@ class Defcon(Cog): def _update_notifier(self) -> None: """Start or stop the notifier according to the DEFCON status.""" - if self.threshold != relativedelta(days=0) and self.expiry is None and not self.defcon_notifier.is_running(): + if self.threshold and self.expiry is None and not self.defcon_notifier.is_running(): log.info("DEFCON notifier started.") self.defcon_notifier.start() - elif (self.threshold == relativedelta(days=0) or self.expiry is not None) and self.defcon_notifier.is_running(): + elif (not self.threshold or self.expiry is not None) and self.defcon_notifier.is_running(): log.info("DEFCON notifier stopped.") self.defcon_notifier.cancel() -- cgit v1.2.3 From b7712cb0c3afac01cc67547ddbe0f17057e07585 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 19 Feb 2021 10:49:59 +0200 Subject: Error to load settings will send the traceback to the channel --- bot/exts/moderation/defcon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 49f5a4ddd..3ea6b971a 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -1,5 +1,6 @@ import asyncio import logging +import traceback from collections import namedtuple from datetime import datetime from enum import Enum @@ -89,6 +90,7 @@ class Defcon(Cog): log.exception("Unable to get DEFCON settings!") await self.channel.send( f"<@&{Roles.moderators}> <@&{Roles.devops}> **WARNING**: Unable to get DEFCON settings!" + f"\n\n```{traceback.format_exc()}```" ) else: -- cgit v1.2.3 From 0370f3677fa74467063f798e32e3728bb1183947 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 19 Feb 2021 10:51:23 +0200 Subject: Retain 'd' alias for threshold command --- bot/exts/moderation/defcon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 3ea6b971a..86dece518 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -156,7 +156,7 @@ class Defcon(Cog): await ctx.send(embed=embed) - @defcon_group.command(aliases=('t',)) + @defcon_group.command(aliases=('t', 'd')) @has_any_role(*MODERATION_ROLES) async def threshold( self, ctx: Context, threshold: Union[DurationDelta, int], expiry: Optional[Expiry] = None -- cgit v1.2.3 From aa5e39c3866a9100fda242221106bf6d2caae38c Mon Sep 17 00:00:00 2001 From: Senjan21 <53477086+Senjan21@users.noreply.github.com> Date: Fri, 19 Feb 2021 10:07:35 +0100 Subject: Delete free.md Reasoning behind this is it is rarely used and when its used its often just to test something or as an attempt to close a help channel. --- bot/resources/tags/free.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 bot/resources/tags/free.md diff --git a/bot/resources/tags/free.md b/bot/resources/tags/free.md deleted file mode 100644 index 1493076c7..000000000 --- a/bot/resources/tags/free.md +++ /dev/null @@ -1,5 +0,0 @@ -**We have a new help channel system!** - -Please see <#704250143020417084> for further information. - -A more detailed guide can be found on [our website](https://pythondiscord.com/pages/resources/guides/help-channels/). -- cgit v1.2.3 From 0f4365e2430d40f17ab9a545d3e8614a4b3a9669 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 20 Feb 2021 11:54:52 +0200 Subject: Remove attachments check in duplicates filter --- bot/rules/duplicates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/rules/duplicates.py b/bot/rules/duplicates.py index 23aefd3dc..8e4fbc12d 100644 --- a/bot/rules/duplicates.py +++ b/bot/rules/duplicates.py @@ -13,7 +13,7 @@ async def apply( if ( msg.author == last_message.author and msg.content == last_message.content - and (msg.content and not msg.attachments) + and msg.content ) ) -- cgit v1.2.3 From 7f980be37a572f1998160ce6a2221504e414d285 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 20 Feb 2021 11:55:09 +0200 Subject: Update CODEOWNERS --- .github/CODEOWNERS | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad813d893..7217cb443 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,11 +7,15 @@ bot/exts/utils/extensions.py @MarkKoz bot/exts/utils/snekbox.py @MarkKoz @Akarys42 bot/exts/help_channels/** @MarkKoz @Akarys42 bot/exts/moderation/** @Akarys42 @mbaruh @Den4200 @ks129 -bot/exts/info/** @Akarys42 @mbaruh @Den4200 +bot/exts/info/** @Akarys42 @Den4200 +bot/exts/info/information.py @mbaruh bot/exts/filters/** @mbaruh bot/exts/fun/** @ks129 bot/exts/utils/** @ks129 +# Rules +bot/rules/** @mbaruh + # Utils bot/utils/extensions.py @MarkKoz bot/utils/function.py @MarkKoz -- cgit v1.2.3 From e3b980e53c13fd5dcaf51408f97c99b629c1a6ec Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 20 Feb 2021 11:55:26 +0200 Subject: Set max attachment from 3 -> 6 --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index d323a946d..e9dce7845 100644 --- a/config-default.yml +++ b/config-default.yml @@ -367,7 +367,7 @@ anti_spam: rules: attachments: interval: 10 - max: 3 + max: 6 burst: interval: 10 -- cgit v1.2.3 From 2268d461f254b4087013ce8ed1125c5287dda099 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Sat, 20 Feb 2021 15:38:58 +0200 Subject: Move reason for guild filtering to footer --- bot/exts/filters/filtering.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 616c40605..2e8552f59 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -388,15 +388,14 @@ class Filtering(Cog): if name == "filter_invites" and match is not True: additional_embeds = [] for _, data in match.items(): - reason = f"\n**Reason:**\n{data['reason']}" if data.get('reason') else "" + reason = f"Reason: {data['reason']} | " if data.get('reason') else "" embed = discord.Embed(description=( f"**Members:**\n{data['members']}\n" f"**Active:**\n{data['active']}" - f"{reason}" )) embed.set_author(name=data["name"]) embed.set_thumbnail(url=data["icon"]) - embed.set_footer(text=f"Guild ID: {data['id']}") + embed.set_footer(text=f"{reason}Guild ID: {data['id']}") additional_embeds.append(embed) elif name == "watch_rich_embeds": -- cgit v1.2.3 From 93c3327414dabd12236e47210be2be1151b71719 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sun, 21 Feb 2021 13:50:37 +0100 Subject: Show the last three characters of censored tokens --- bot/exts/filters/token_remover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/token_remover.py b/bot/exts/filters/token_remover.py index bd6a1f97a..33b39cc2d 100644 --- a/bot/exts/filters/token_remover.py +++ b/bot/exts/filters/token_remover.py @@ -147,7 +147,7 @@ class TokenRemover(Cog): channel=msg.channel.mention, user_id=token.user_id, timestamp=token.timestamp, - hmac='x' * len(token.hmac), + hmac='x' * (len(token.hmac) - 3) + token.hmac[-3:], ) @classmethod -- cgit v1.2.3 From 04e233685e163d1e513a21acd236c2385536b0b7 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sun, 21 Feb 2021 13:51:57 +0100 Subject: Ping the mods if a token present in the server is found no matter the kind --- bot/exts/filters/token_remover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/token_remover.py b/bot/exts/filters/token_remover.py index 33b39cc2d..93f1f3c33 100644 --- a/bot/exts/filters/token_remover.py +++ b/bot/exts/filters/token_remover.py @@ -135,7 +135,7 @@ class TokenRemover(Cog): user_id=user_id, user_name=str(user), kind="BOT" if user.bot else "USER", - ), not user.bot + ), True else: return UNKNOWN_USER_LOG_MESSAGE.format(user_id=user_id), False -- cgit v1.2.3 From 27e60e94bd1fe6784c2b7674433bb175255fa217 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sun, 21 Feb 2021 14:06:54 +0100 Subject: Update token remover unittests --- tests/bot/exts/filters/test_token_remover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bot/exts/filters/test_token_remover.py b/tests/bot/exts/filters/test_token_remover.py index f99cc3370..51feae9cb 100644 --- a/tests/bot/exts/filters/test_token_remover.py +++ b/tests/bot/exts/filters/test_token_remover.py @@ -291,7 +291,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase): channel=self.msg.channel.mention, user_id=token.user_id, timestamp=token.timestamp, - hmac="x" * len(token.hmac), + hmac="xxxxxxxxxxxxxxxxxxxxxxxxjf4", ) @autospec("bot.exts.filters.token_remover", "UNKNOWN_USER_LOG_MESSAGE") @@ -318,7 +318,7 @@ class TokenRemoverTests(unittest.IsolatedAsyncioTestCase): return_value = TokenRemover.format_userid_log_message(msg, token) - self.assertEqual(return_value, (known_user_log_message.format.return_value, False)) + self.assertEqual(return_value, (known_user_log_message.format.return_value, True)) known_user_log_message.format.assert_called_once_with( user_id=472265943062413332, -- cgit v1.2.3 From 584ed52c7107c7d3e3b838ee1e8df3a22ae95e35 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 21 Feb 2021 23:07:38 +0000 Subject: Update max available channels to 3 Partially resolves #1427 --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index beaf89f2c..8e9a29a51 100644 --- a/config-default.yml +++ b/config-default.yml @@ -470,7 +470,7 @@ help_channels: deleted_idle_minutes: 5 # Maximum number of channels to put in the available category - max_available: 2 + max_available: 3 # Maximum number of channels across all 3 categories # Note Discord has a hard limit of 50 channels per category, so this shouldn't be > 50 -- cgit v1.2.3 From 1daf01ef9a3853252d4cadab5fc6abce14df3557 Mon Sep 17 00:00:00 2001 From: wookie184 Date: Mon, 22 Feb 2021 11:35:12 +0000 Subject: Rewrite inline codeblock tag --- bot/resources/tags/inline.md | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/bot/resources/tags/inline.md b/bot/resources/tags/inline.md index a6a7c35d6..4ece74ef7 100644 --- a/bot/resources/tags/inline.md +++ b/bot/resources/tags/inline.md @@ -1,16 +1,7 @@ **Inline codeblocks** -In addition to multi-line codeblocks, discord has support for inline codeblocks as well. These are small codeblocks that are usually a single line, that can fit between non-codeblocks on the same line. +Inline codeblocks look `like this`. To create them you surround text with single backticks, so \`hello\` would become `hello`. -The following is an example of how it's done: +Note that backticks are not quotes, see [this](https://superuser.com/questions/254076/how-do-i-type-the-tick-and-backtick-characters-on-windows/254077#254077) if you are struggling to find the backtick key. -The \`\_\_init\_\_\` method customizes the newly created instance. - -And results in the following: - -The `__init__` method customizes the newly created instance. - -**Note:** -• These are **backticks** not quotes -• Avoid using them for multiple lines -• Useful for negating formatting you don't want +For how to make multiline codeblocks see the `!codeblock` tag. -- cgit v1.2.3 From b116688be7d8b3d83c88a78969e2118e0504fadc Mon Sep 17 00:00:00 2001 From: wookie184 Date: Mon, 22 Feb 2021 11:38:45 +0000 Subject: Add pep 8 song to pep 8 tag --- bot/resources/tags/pep8.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/pep8.md b/bot/resources/tags/pep8.md index cab4c4db8..57b176122 100644 --- a/bot/resources/tags/pep8.md +++ b/bot/resources/tags/pep8.md @@ -1,3 +1,5 @@ -**PEP 8** is the official style guide for Python. It includes comprehensive guidelines for code formatting, variable naming, and making your code easy to read. Professional Python developers are usually required to follow the guidelines, and will often use code-linters like `flake8` to verify that the code they\'re writing complies with the style guide. +**PEP 8** is the official style guide for Python. It includes comprehensive guidelines for code formatting, variable naming, and making your code easy to read. Professional Python developers are usually required to follow the guidelines, and will often use code-linters like flake8 to verify that the code they're writing complies with the style guide. -You can find the PEP 8 document [here](https://www.python.org/dev/peps/pep-0008). +More information: +• [PEP 8 document](https://www.python.org/dev/peps/pep-0008) +• [Our PEP 8 song!](https://www.youtube.com/watch?v=hgI0p1zf31k) :notes: -- cgit v1.2.3 From 0b11d7dfb408f4e5fe6248ae8377ddc7aa1aa5ee Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Tue, 23 Feb 2021 03:48:35 +0100 Subject: Add truncate_message util --- bot/utils/messages.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/utils/messages.py b/bot/utils/messages.py index 077dd9569..c01fa5d0e 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -154,3 +154,12 @@ async def send_denial(ctx: Context, reason: str) -> None: def format_user(user: discord.abc.User) -> str: """Return a string for `user` which has their mention and ID.""" return f"{user.mention} (`{user.id}`)" + + +def truncate_message(message: discord.Message, limit: int) -> str: + """Returns a truncated version of the message content, up to the specified limit.""" + text = message.content + if len(text) > limit: + return text[:limit-3] + "..." + else: + return text -- cgit v1.2.3 From e1d269d82eed8a01d3d3b0ff33d05e3c79324007 Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Tue, 23 Feb 2021 04:00:01 +0100 Subject: Add function to DM users when opening help channel --- bot/exts/help_channels/_message.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 2bbd4bdd6..12ac4035d 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -8,6 +8,7 @@ import bot from bot import constants from bot.exts.help_channels import _caches from bot.utils.channel import is_in_category +from bot.utils.messages import truncate_message log = logging.getLogger(__name__) @@ -92,6 +93,38 @@ async def is_empty(channel: discord.TextChannel) -> bool: return False +async def dm_on_open(message: discord.Message) -> None: + """ + DM claimant with a link to the claimed channel's first message, with a 100 letter preview of the message. + + Does nothing if the user has DMs disabled. + """ + embed = discord.Embed( + title="Help channel opened", + description=f"You claimed {message.channel.mention}.", + colour=bot.constants.Colours.bright_green, + timestamp=message.created_at, + ) + + embed.set_thumbnail(url=constants.Icons.green_questionmark) + embed.add_field( + name="Your message", value=truncate_message(message, limit=100), inline=False + ) + embed.add_field( + name="Want to go there?", + value=f"[Jump to message!]({message.jump_url})", + inline=False, + ) + + try: + await message.author.send(embed=embed) + log.trace(f"Sent DM to {message.author.id} after claiming help channel.") + except discord.errors.Forbidden: + log.trace( + f"Ignoring to send DM to {message.author.id} after claiming help channel: DMs disabled." + ) + + async def notify(channel: discord.TextChannel, last_notification: t.Optional[datetime]) -> t.Optional[datetime]: """ Send a message in `channel` notifying about a lack of available help channels. -- cgit v1.2.3 From e6483d633ac6ecc2a88051442108d9c88e5f7745 Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Tue, 23 Feb 2021 04:00:58 +0100 Subject: Add green question mark to default config Add green question mark to default config Add green question mark to config --- bot/constants.py | 1 + config-default.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 8a93ff9cf..69bc82b89 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -326,6 +326,7 @@ class Icons(metaclass=YAMLGetter): filtering: str green_checkmark: str + green_questionmark: str guild_update: str hash_blurple: str diff --git a/config-default.yml b/config-default.yml index 8e9a29a51..7d9afaa0e 100644 --- a/config-default.yml +++ b/config-default.yml @@ -90,6 +90,7 @@ style: filtering: "https://cdn.discordapp.com/emojis/472472638594482195.png" green_checkmark: "https://raw.githubusercontent.com/python-discord/branding/master/icons/checkmark/green-checkmark-dist.png" + green_questionmark: "https://raw.githubusercontent.com/python-discord/branding/master/icons/checkmark/green-question-mark-dist.png" guild_update: "https://cdn.discordapp.com/emojis/469954765141442561.png" hash_blurple: "https://cdn.discordapp.com/emojis/469950142942806017.png" -- cgit v1.2.3 From e34ea2f1c108d1900e251d17b38563536345d2de Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Tue, 23 Feb 2021 04:07:05 +0100 Subject: Send DM when user claims help channel --- bot/exts/help_channels/_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 0995c8a79..a18ddc900 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -102,6 +102,7 @@ class HelpChannels(commands.Cog): await _cooldown.revoke_send_permissions(message.author, self.scheduler) await _message.pin(message) + await _message.dm_on_open(message) # Add user with channel for dormant check. await _caches.claimants.set(message.channel.id, message.author.id) -- cgit v1.2.3 From bb9e56c3cb874ef76ab82db02ce8242117e0da92 Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Tue, 23 Feb 2021 11:08:41 +0100 Subject: Update embed field title to be more formal --- bot/exts/help_channels/_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 12ac4035d..95aca067a 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -111,7 +111,7 @@ async def dm_on_open(message: discord.Message) -> None: name="Your message", value=truncate_message(message, limit=100), inline=False ) embed.add_field( - name="Want to go there?", + name="Conversation", value=f"[Jump to message!]({message.jump_url})", inline=False, ) -- cgit v1.2.3 From cae0d84757e026976f1a9e87d52c581669b7b8e8 Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Tue, 23 Feb 2021 11:14:31 +0100 Subject: Use textwrap.shorten instead of custom function This applies to the help channel DM embed, where the user is sent a truncated version of their message. --- bot/exts/help_channels/_message.py | 6 ++++-- bot/utils/messages.py | 9 --------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 95aca067a..4113e51c5 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -1,4 +1,5 @@ import logging +import textwrap import typing as t from datetime import datetime @@ -8,7 +9,6 @@ import bot from bot import constants from bot.exts.help_channels import _caches from bot.utils.channel import is_in_category -from bot.utils.messages import truncate_message log = logging.getLogger(__name__) @@ -108,7 +108,9 @@ async def dm_on_open(message: discord.Message) -> None: embed.set_thumbnail(url=constants.Icons.green_questionmark) embed.add_field( - name="Your message", value=truncate_message(message, limit=100), inline=False + name="Your message", + value=textwrap.shorten(message.content, width=100, placeholder="..."), + inline=False, ) embed.add_field( name="Conversation", diff --git a/bot/utils/messages.py b/bot/utils/messages.py index c01fa5d0e..077dd9569 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -154,12 +154,3 @@ async def send_denial(ctx: Context, reason: str) -> None: def format_user(user: discord.abc.User) -> str: """Return a string for `user` which has their mention and ID.""" return f"{user.mention} (`{user.id}`)" - - -def truncate_message(message: discord.Message, limit: int) -> str: - """Returns a truncated version of the message content, up to the specified limit.""" - text = message.content - if len(text) > limit: - return text[:limit-3] + "..." - else: - return text -- cgit v1.2.3 From d71ac9f6e240ffd2d4195d9dbbf5740a0c2413a1 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 23 Feb 2021 19:24:18 +0300 Subject: Fixes Problems With Help Channel DM Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/exts/help_channels/_cog.py | 5 ++++- bot/exts/help_channels/_message.py | 8 +++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index a18ddc900..6abf99810 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -102,7 +102,10 @@ class HelpChannels(commands.Cog): await _cooldown.revoke_send_permissions(message.author, self.scheduler) await _message.pin(message) - await _message.dm_on_open(message) + try: + await _message.dm_on_open(message) + except Exception as e: + log.warning("Error occurred while sending DM:", exc_info=e) # Add user with channel for dormant check. await _caches.claimants.set(message.channel.id, message.author.id) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 4113e51c5..36388f9bd 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -107,11 +107,9 @@ async def dm_on_open(message: discord.Message) -> None: ) embed.set_thumbnail(url=constants.Icons.green_questionmark) - embed.add_field( - name="Your message", - value=textwrap.shorten(message.content, width=100, placeholder="..."), - inline=False, - ) + formatted_message = textwrap.shorten(message.content, width=100, placeholder="...") + if formatted_message: + embed.add_field(name="Your message", value=formatted_message, inline=False) embed.add_field( name="Conversation", value=f"[Jump to message!]({message.jump_url})", -- cgit v1.2.3 From c29cda2e96c73518517acefa77e967f791ec87c5 Mon Sep 17 00:00:00 2001 From: ks129 <45097959+ks129@users.noreply.github.com> Date: Wed, 24 Feb 2021 09:36:52 +0200 Subject: Remove compatibility-none returns from filters This doesn't look good to have just None returns, so made changes so if function don't return tuple, then reason is automatically None. --- bot/exts/filters/filtering.py | 51 ++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 2e8552f59..4093ba4ad 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -271,9 +271,17 @@ class Filtering(Cog): # Does the filter only need the message content or the full message? if _filter["content_only"]: - match, reason = await _filter["function"](msg.content) + payload = msg.content else: - match, reason = await _filter["function"](msg) + payload = msg + + result = await _filter["function"](payload) + reason = None + + if isinstance(result, tuple): + match, reason = result + else: + match = result if match: is_private = msg.channel.type is discord.ChannelType.private @@ -461,16 +469,15 @@ class Filtering(Cog): return False, None @staticmethod - async def _has_zalgo(text: str) -> Tuple[bool, None]: + async def _has_zalgo(text: str) -> bool: """ Returns True if the text contains zalgo characters. Zalgo range is \u0300 – \u036F and \u0489. - Return None as second value for compability with other filters. """ - return bool(ZALGO_RE.search(text)), None + return bool(ZALGO_RE.search(text)) - async def _has_invites(self, text: str) -> Tuple[Union[dict, bool], None]: + async def _has_invites(self, text: str) -> Union[dict, bool]: """ Checks if there's any invites in the text content that aren't in the guild whitelist. @@ -478,8 +485,6 @@ class Filtering(Cog): If none are detected, False is returned. Attempts to catch some of common ways to try to cheat the system. - - Return None as second value for compability with other filters. """ # Remove backslashes to prevent escape character aroundfuckery like # discord\.gg/gdudes-pony-farm @@ -500,7 +505,7 @@ class Filtering(Cog): # Lack of a "guild" key in the JSON response indicates either an group DM invite, an # expired invite, or an invalid invite. The API does not currently differentiate # between invalid and expired invites - return True, None + return True guild_id = guild.get("id") guild_invite_whitelist = self._get_filterlist_items("guild_invite", allowed=True) @@ -537,15 +542,11 @@ class Filtering(Cog): "reason": reason } - return invite_data if invite_data else False, None + return invite_data if invite_data else False @staticmethod - async def _has_rich_embed(msg: Message) -> Tuple[Union[bool, List[discord.Embed]], None]: - """ - Determines if `msg` contains any rich embeds not auto-generated from a URL. - - Return None as second value for compability with other filters. - """ + async def _has_rich_embed(msg: Message) -> Union[bool, List[discord.Embed]]: + """Determines if `msg` contains any rich embeds not auto-generated from a URL.""" if msg.embeds: for embed in msg.embeds: if embed.type == "rich": @@ -553,28 +554,24 @@ class Filtering(Cog): if not embed.url or embed.url not in urls: # If `embed.url` does not exist or if `embed.url` is not part of the content # of the message, it's unlikely to be an auto-generated embed by Discord. - return msg.embeds, None + return msg.embeds else: log.trace( "Found a rich embed sent by a regular user account, " "but it was likely just an automatic URL embed." ) - return False, None - return False, None + return False + return False @staticmethod - async def _has_everyone_ping(text: str) -> Tuple[bool, None]: - """ - Determines if `msg` contains an @everyone or @here ping outside of a codeblock. - - Return None as second value for compability with other filters. - """ + async def _has_everyone_ping(text: str) -> bool: + """Determines if `msg` contains an @everyone or @here ping outside of a codeblock.""" # First pass to avoid running re.sub on every message if not EVERYONE_PING_RE.search(text): - return False, None + return False content_without_codeblocks = CODE_BLOCK_RE.sub("", text) - return bool(EVERYONE_PING_RE.search(content_without_codeblocks)), None + return bool(EVERYONE_PING_RE.search(content_without_codeblocks)) async def notify_member(self, filtered_member: Member, reason: str, channel: TextChannel) -> None: """ -- cgit v1.2.3 From 44eb00ca03dae1b3d5faf40be63fae04ca515790 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Wed, 24 Feb 2021 18:27:25 +0100 Subject: Add off-topic etiquette to the off-topic tag --- bot/resources/tags/off-topic.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/resources/tags/off-topic.md b/bot/resources/tags/off-topic.md index c7f98a813..6a864a1d5 100644 --- a/bot/resources/tags/off-topic.md +++ b/bot/resources/tags/off-topic.md @@ -6,3 +6,5 @@ There are three off-topic channels: • <#463035268514185226> Their names change randomly every 24 hours, but you can always find them under the `OFF-TOPIC/GENERAL` category in the channel list. + +Please read our [off-topic etiquette](https://pythondiscord.com/pages/resources/guides/off-topic-etiquette/) before participating in conversations. -- cgit v1.2.3 From 3153ad05222156b4aacb0f288d36ec3a1ab6eda1 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 25 Feb 2021 04:23:54 +0100 Subject: Send command help for `BadUnionArgument`s --- bot/exts/backend/error_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index d2cce5558..92414e5d2 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -239,6 +239,7 @@ class ErrorHandler(Cog): elif isinstance(e, errors.BadUnionArgument): embed = self._get_error_embed("Bad argument", f"{e}\n{e.errors[-1]}") await ctx.send(embed=embed) + await prepared_help_command self.bot.stats.incr("errors.bad_union_argument") elif isinstance(e, errors.ArgumentParsingError): embed = self._get_error_embed("Argument parsing error", str(e)) -- cgit v1.2.3 From d56b01ad1d9d2b52603255978061c95a2487505a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 25 Feb 2021 04:24:20 +0100 Subject: Close coroutine to prevent `RuntimeWarning`s --- bot/exts/backend/error_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index 92414e5d2..9cb54cdab 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -244,6 +244,7 @@ class ErrorHandler(Cog): elif isinstance(e, errors.ArgumentParsingError): embed = self._get_error_embed("Argument parsing error", str(e)) await ctx.send(embed=embed) + prepared_help_command.close() self.bot.stats.incr("errors.argument_parsing_error") else: embed = self._get_error_embed( -- cgit v1.2.3 From c5e113734d16e8d5ac2eede6c1f29e019cfc2f28 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 25 Feb 2021 12:58:15 +0300 Subject: Adds More Descriptive Startup Error Messages Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/__main__.py | 24 ++++++++++++++++++++---- bot/bot.py | 13 ++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/bot/__main__.py b/bot/__main__.py index 257216fa7..e4df4b77d 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -1,10 +1,26 @@ +import logging + +import aiohttp + import bot from bot import constants -from bot.bot import Bot +from bot.bot import Bot, StartupError from bot.log import setup_sentry setup_sentry() -bot.instance = Bot.create() -bot.instance.load_extensions() -bot.instance.run(constants.Bot.token) +try: + bot.instance = Bot.create() + bot.instance.load_extensions() + bot.instance.run(constants.Bot.token) +except StartupError as e: + message = "Unknown Startup Error Occurred." + if isinstance(e.exception, aiohttp.ClientConnectorError): + message = "Could not connect to site API. Is it running?" + elif isinstance(e.exception, OSError): + message = "Could not connect to Redis. Is it running?" + + # The exception is logged with an empty message so the actual message is visible at the bottom + log = logging.getLogger("bot") + log.fatal("", exc_info=e.exception) + log.fatal(message) diff --git a/bot/bot.py b/bot/bot.py index d5f108575..df80868ee 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -19,6 +19,14 @@ log = logging.getLogger('bot') LOCALHOST = "127.0.0.1" +class StartupError(Exception): + """Exception class for startup errors.""" + + def __init__(self, base: Exception): + super() + self.exception = base + + class Bot(commands.Bot): """A subclass of `discord.ext.commands.Bot` with an aiohttp session and an API client.""" @@ -318,5 +326,8 @@ def _create_redis_session(loop: asyncio.AbstractEventLoop) -> RedisSession: use_fakeredis=constants.Redis.use_fakeredis, global_namespace="bot", ) - loop.run_until_complete(redis_session.connect()) + try: + loop.run_until_complete(redis_session.connect()) + except OSError as e: + raise StartupError(e) return redis_session -- cgit v1.2.3 From 866f3156cb05e49a8ca2c9ebdb13688829f15914 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 25 Feb 2021 12:59:37 +0300 Subject: Adds Site Readiness Checks Attempts to connect to the site multiple times before throwing an exception to allow the site to warm up when running in docker. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/bot.py | 21 +++++++++++++++++++++ bot/constants.py | 2 ++ config-default.yml | 2 ++ 3 files changed, 25 insertions(+) diff --git a/bot/bot.py b/bot/bot.py index df80868ee..cd8e26325 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -89,6 +89,22 @@ class Bot(commands.Bot): for item in full_cache: self.insert_item_into_filter_list_cache(item) + async def ping_services(self) -> None: + """A helper to make sure all the services the bot relies on are available on startup.""" + # Connect Site/API + attempts = 0 + while True: + try: + log.info(f"Attempting site connection: {attempts + 1}/{constants.URLs.connect_max_retries}") + await self.api_client.get("healthcheck") + break + + except aiohttp.ClientConnectorError as e: + attempts += 1 + if attempts == constants.URLs.connect_max_retries: + raise e + await asyncio.sleep(constants.URLs.connect_cooldown) + @classmethod def create(cls) -> "Bot": """Create and return an instance of a Bot.""" @@ -231,6 +247,11 @@ class Bot(commands.Bot): # here. Normally, this shouldn't happen. await self.redis_session.connect() + try: + await self.ping_services() + except Exception as e: + raise StartupError(e) + # Build the FilterList cache await self.cache_filter_list_data() diff --git a/bot/constants.py b/bot/constants.py index 69bc82b89..7cf31e835 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -531,6 +531,8 @@ class URLs(metaclass=YAMLGetter): github_bot_repo: str # Base site vars + connect_max_retries: int + connect_cooldown: int site: str site_api: str site_schema: str diff --git a/config-default.yml b/config-default.yml index 7d9afaa0e..a9fb2262e 100644 --- a/config-default.yml +++ b/config-default.yml @@ -338,6 +338,8 @@ keys: urls: # PyDis site vars + connect_max_retries: 3 + connect_cooldown: 5 site: &DOMAIN "pythondiscord.com" site_api: &API "pydis-api.default.svc.cluster.local" site_api_schema: "http://" -- cgit v1.2.3 From 900923cc6a9b4d40b625b8f33e8bef18a286a84f Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:23:14 +0300 Subject: Catches All Site Startup Issues Adds a missing exception when trying to connect to the site on startup. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/__main__.py | 2 +- bot/bot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/__main__.py b/bot/__main__.py index e4df4b77d..d3abcd7b2 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -15,7 +15,7 @@ try: bot.instance.run(constants.Bot.token) except StartupError as e: message = "Unknown Startup Error Occurred." - if isinstance(e.exception, aiohttp.ClientConnectorError): + if type(e.exception) in [aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError]: message = "Could not connect to site API. Is it running?" elif isinstance(e.exception, OSError): message = "Could not connect to Redis. Is it running?" diff --git a/bot/bot.py b/bot/bot.py index cd8e26325..1a815c31e 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -99,7 +99,7 @@ class Bot(commands.Bot): await self.api_client.get("healthcheck") break - except aiohttp.ClientConnectorError as e: + except (aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError) as e: attempts += 1 if attempts == constants.URLs.connect_max_retries: raise e -- cgit v1.2.3 From 4c566bb2445d0bc637e11242c44a69baa8a39e48 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:42:22 +0300 Subject: Cleans Up Startup Error Handler Code Style Co-authored-by: Akarys42 Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/__main__.py | 4 +++- bot/bot.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/__main__.py b/bot/__main__.py index d3abcd7b2..9317563c8 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -15,7 +15,7 @@ try: bot.instance.run(constants.Bot.token) except StartupError as e: message = "Unknown Startup Error Occurred." - if type(e.exception) in [aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError]: + if isinstance(e.exception, (aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError)): message = "Could not connect to site API. Is it running?" elif isinstance(e.exception, OSError): message = "Could not connect to Redis. Is it running?" @@ -24,3 +24,5 @@ except StartupError as e: log = logging.getLogger("bot") log.fatal("", exc_info=e.exception) log.fatal(message) + + exit(69) diff --git a/bot/bot.py b/bot/bot.py index 1a815c31e..3218a60b4 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -102,7 +102,7 @@ class Bot(commands.Bot): except (aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError) as e: attempts += 1 if attempts == constants.URLs.connect_max_retries: - raise e + raise await asyncio.sleep(constants.URLs.connect_cooldown) @classmethod -- cgit v1.2.3 From 283857f543ca50e188f39a9b880cef9963f486db Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:44:18 +0300 Subject: Call Super __init__ in Startup Error Co-authored-by: Matteo Bertucci --- bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/bot.py b/bot/bot.py index 3218a60b4..1b4037076 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -23,7 +23,7 @@ class StartupError(Exception): """Exception class for startup errors.""" def __init__(self, base: Exception): - super() + super().__init__() self.exception = base -- cgit v1.2.3 From fb7e21a0897e6de4964ff883f1cd52a9dd443722 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Thu, 25 Feb 2021 13:48:52 +0300 Subject: Removes Unused Variable Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- bot/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/bot.py b/bot/bot.py index 1b4037076..3a2af472d 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -99,7 +99,7 @@ class Bot(commands.Bot): await self.api_client.get("healthcheck") break - except (aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError) as e: + except (aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError): attempts += 1 if attempts == constants.URLs.connect_max_retries: raise -- cgit v1.2.3 From ad2bc5d2d1d94ac3ef60d9b60e6f716be5827bf2 Mon Sep 17 00:00:00 2001 From: Sebastian Kuipers <61157793+sebkuip@users.noreply.github.com> Date: Thu, 25 Feb 2021 17:17:00 +0100 Subject: Apply suggestions from code review Co-authored-by: Mark --- bot/resources/tags/empty-json.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/resources/tags/empty-json.md b/bot/resources/tags/empty-json.md index 93e2cadba..935544bb7 100644 --- a/bot/resources/tags/empty-json.md +++ b/bot/resources/tags/empty-json.md @@ -4,8 +4,8 @@ JSONDecodeError: Expecting value: line 1 column 1 (char 0) ``` This error could have appeared because you just created the JSON file and there is nothing in it at the moment. -Whilst having the data empty is no problem, the file itself may never be completely empty. +Whilst having empty data is no problem, the file itself may never be completely empty. -You most likely wanted to structure your JSON as a dictionary. To do this, change your JSON to read `{}`. +You most likely wanted to structure your JSON as a dictionary. To do this, edit your empty JSON file so that it instead contains `{}`. Different data types are also supported. If you wish to read more on these, please refer to [this article](https://www.tutorialspoint.com/json/json_data_types.htm). -- cgit v1.2.3 From 82190ee57bd25a1e999b7a8fb323513696e7e042 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 26 Feb 2021 02:25:18 +0000 Subject: Bump aiohttp from 3.7.3 to 3.7.4 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.3 to 3.7.4. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.3...v3.7.4) Signed-off-by: dependabot[bot] --- Pipfile | 2 +- Pipfile.lock | 282 ++++++++++++++++++++++++++++------------------------------- 2 files changed, 133 insertions(+), 151 deletions(-) diff --git a/Pipfile b/Pipfile index efdd46522..0a94fb888 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] aio-pika = "~=6.1" aiodns = "~=2.0" -aiohttp = "~=3.5" +aiohttp = "~=3.7" aioping = "~=0.3.1" aioredis = "~=1.3.1" "async-rediscache[fakeredis]" = "~=0.1.2" diff --git a/Pipfile.lock b/Pipfile.lock index 636d07b1a..f8cedb08f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "26c8089f17d6d6bac11dbed366b1b46818b4546f243af756a106a32af5d9d8f6" + "sha256": "228ae55fe5700ac3827ba6b661933b60b1d06f44fea8bcbe8c5a769fa10ab2fd" }, "pipfile-spec": 6, "requires": { @@ -34,46 +34,46 @@ }, "aiohttp": { "hashes": [ - "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", - "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", - "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", - "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", - "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", - "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", - "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", - "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", - "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", - "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", - "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", - "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", - "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", - "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", - "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", - "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", - "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", - "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", - "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", - "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", - "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", - "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", - "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", - "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", - "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", - "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", - "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", - "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", - "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", - "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", - "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", - "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", - "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", - "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", - "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", - "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", - "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" + "sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0", + "sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6", + "sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf", + "sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9", + "sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e", + "sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0", + "sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329", + "sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2", + "sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40", + "sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a", + "sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4", + "sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de", + "sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9", + "sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9", + "sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb", + "sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076", + "sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de", + "sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907", + "sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d", + "sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536", + "sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d", + "sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54", + "sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc", + "sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212", + "sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9", + "sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d", + "sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b", + "sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7", + "sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81", + "sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c", + "sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895", + "sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297", + "sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb", + "sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe", + "sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242", + "sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0", + "sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2" ], "index": "pypi", - "version": "==3.7.3" + "version": "==3.7.4" }, "aioping": { "hashes": [ @@ -96,7 +96,6 @@ "sha256:8218dd9f7198d6e7935855468326bbacf0089f926c70baa8dd92944cb2496573", "sha256:e584dac13a242589aaf42470fd3006cb0dc5aed6506cbd20357c7ec8bbe4a89e" ], - "markers": "python_version >= '3.6'", "version": "==3.3.1" }, "alabaster": { @@ -123,7 +122,6 @@ "sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af" ], "index": "pypi", - "markers": "python_version ~= '3.7'", "version": "==0.1.4" }, "async-timeout": { @@ -131,7 +129,6 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], - "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -139,7 +136,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "babel": { @@ -147,7 +143,6 @@ "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0" }, "beautifulsoup4": { @@ -168,44 +163,45 @@ }, "cffi": { "hashes": [ - "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", - "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", - "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", - "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", - "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", - "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", - "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", - "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", - "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", - "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", - "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", - "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", - "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", - "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", - "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", - "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", - "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", - "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", - "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", - "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", - "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", - "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", - "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", - "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", - "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", - "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", - "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", - "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", - "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", - "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", - "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", - "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", - "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", - "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", - "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", - "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" - ], - "version": "==1.14.4" + "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813", + "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06", + "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea", + "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee", + "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396", + "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73", + "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315", + "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1", + "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49", + "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892", + "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482", + "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058", + "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5", + "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53", + "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045", + "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3", + "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5", + "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e", + "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c", + "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369", + "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827", + "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053", + "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa", + "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4", + "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322", + "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132", + "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62", + "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa", + "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0", + "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396", + "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e", + "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991", + "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6", + "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1", + "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406", + "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d", + "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c" + ], + "version": "==1.14.5" }, "chardet": { "hashes": [ @@ -219,6 +215,7 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], + "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==0.4.4" }, @@ -251,7 +248,6 @@ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, "emoji": { @@ -334,7 +330,6 @@ "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "humanfriendly": { @@ -342,7 +337,6 @@ "sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d", "sha256:d5c731705114b9ad673754f3317d9fa4c23212f36b29bdc4272a892eafc9bc72" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==9.1" }, "idna": { @@ -350,7 +344,6 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "imagesize": { @@ -358,16 +351,14 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "jinja2": { "hashes": [ - "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", - "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" + "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", + "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.11.2" + "version": "==2.11.3" }, "lxml": { "hashes": [ @@ -427,8 +418,12 @@ "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", + "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", + "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", @@ -437,26 +432,40 @@ "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", + "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", + "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", + "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", + "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", + "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", + "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "more-itertools": { @@ -507,23 +516,20 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], - "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "ordered-set": { "hashes": [ "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95" ], - "markers": "python_version >= '3.5'", "version": "==4.0.2" }, "packaging": { "hashes": [ - "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858", - "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093" + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.8" + "version": "==20.9" }, "pamqp": { "hashes": [ @@ -571,23 +577,20 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pygments": { "hashes": [ - "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", - "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" + "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", + "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" ], - "markers": "python_version >= '3.5'", - "version": "==2.7.4" + "version": "==2.8.0" }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.4.7" }, "python-dateutil": { @@ -600,10 +603,10 @@ }, "pytz": { "hashes": [ - "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", - "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5" + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" ], - "version": "==2020.5" + "version": "==2021.1" }, "pyyaml": { "hashes": [ @@ -629,7 +632,6 @@ "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.5.3" }, "requests": { @@ -653,15 +655,14 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "snowballstemmer": { "hashes": [ - "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", - "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" + "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", + "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" ], - "version": "==2.0.0" + "version": "==2.1.0" }, "sortedcontainers": { "hashes": [ @@ -672,11 +673,11 @@ }, "soupsieve": { "hashes": [ - "sha256:4bb21a6ee4707bf43b61230e80740e71bfe56e55d1f1f50924b087bb2975c851", - "sha256:6dc52924dc0bc710a5d16794e6b3480b2c7c08b07729505feab2b2c16661ff6e" + "sha256:407fa1e8eb3458d1b5614df51d9651a1180ea5fedf07feb46e45d7e25e6d6cdd", + "sha256:d3a5ea5b350423f47d07639f74475afedad48cf41c0ad7a82ca13a3928af34f6" ], "markers": "python_version >= '3.0'", - "version": "==2.1" + "version": "==2.2" }, "sphinx": { "hashes": [ @@ -691,7 +692,6 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -699,7 +699,6 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -707,7 +706,6 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -715,7 +713,6 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], - "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -723,7 +720,6 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -731,7 +727,6 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], - "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "statsd": { @@ -752,11 +747,10 @@ }, "urllib3": { "hashes": [ - "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", - "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.2" + "version": "==1.26.3" }, "yarl": { "hashes": [ @@ -798,7 +792,6 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "markers": "python_version >= '3.6'", "version": "==1.6.3" } }, @@ -815,7 +808,6 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "certifi": { @@ -830,7 +822,6 @@ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], - "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -995,18 +986,16 @@ }, "identify": { "hashes": [ - "sha256:18994e850ba50c37bcaed4832be8b354d6a06c8fb31f54e0e7ece76d32f69bc8", - "sha256:892473bf12e655884132a3a32aca737a3cbefaa34a850ff52d501773a45837bc" + "sha256:de7129142a5c86d75a52b96f394d94d96d497881d2aaf8eafe320cdbe8ac4bcc", + "sha256:e0dae57c0397629ce13c289f6ddde0204edf518f557bfdb1e56474aa143e77c3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.5.12" + "version": "==1.5.14" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "mccabe": { @@ -1044,7 +1033,6 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -1052,7 +1040,6 @@ "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], - "markers": "python_version >= '3.5'", "version": "==5.1.1" }, "pyflakes": { @@ -1060,7 +1047,6 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pyyaml": { @@ -1095,39 +1081,35 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.15.0" }, "snowballstemmer": { "hashes": [ - "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0", - "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52" + "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", + "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" ], - "version": "==2.0.0" + "version": "==2.1.0" }, "toml": { "hashes": [ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", "version": "==0.10.2" }, "urllib3": { "hashes": [ - "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", - "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.2" + "version": "==1.26.3" }, "virtualenv": { "hashes": [ - "sha256:0c111a2236b191422b37fe8c28b8c828ced39aab4bf5627fa5c331aeffb570d9", - "sha256:14b34341e742bdca219e10708198e704e8a7064dd32f474fc16aca68ac53a306" + "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d", + "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.1" + "version": "==20.4.2" } } } -- cgit v1.2.3 From cea82da9547d3178f071241a75d024582d314ff9 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 26 Feb 2021 14:48:10 +0200 Subject: Supressing any exceptions while updating the threshold in redis Updating redis might cause an error, making sure it doesn't stop the command mid-way --- bot/exts/moderation/defcon.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 86dece518..a88892b13 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -2,6 +2,7 @@ import asyncio import logging import traceback from collections import namedtuple +from contextlib import suppress from datetime import datetime from enum import Enum from typing import Optional, Union @@ -208,12 +209,13 @@ class Defcon(Cog): if self.expiry is not None: self.scheduler.schedule_at(expiry, 0, self._remove_threshold()) - await self.defcon_settings.update( - { - 'threshold': Defcon._stringify_relativedelta(self.threshold) if self.threshold else "", - 'expiry': expiry.isoformat() if expiry else 0 - } - ) + with suppress(Exception): + await self.defcon_settings.update( + { + 'threshold': Defcon._stringify_relativedelta(self.threshold) if self.threshold else "", + 'expiry': expiry.isoformat() if expiry else 0 + } + ) self._update_notifier() action = Action.DURATION_UPDATE -- cgit v1.2.3 From 80153ed12d20ccaa637a55765df60d8d3b5e64ef Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 26 Feb 2021 15:17:16 +0200 Subject: Changed name of _duration_parser constant to uppercase --- bot/utils/time.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index a7b441327..f862e40f7 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -9,7 +9,7 @@ from dateutil.relativedelta import relativedelta RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" INFRACTION_FORMAT = "%Y-%m-%d %H:%M" -_duration_parser = re.compile( +_DURATION_REGEX = re.compile( r"((?P\d+?) ?(years|year|Y|y) ?)?" r"((?P\d+?) ?(months|month|m) ?)?" r"((?P\d+?) ?(weeks|week|W|w) ?)?" @@ -100,7 +100,7 @@ def parse_duration_string(duration: str) -> Optional[relativedelta]: The units need to be provided in descending order of magnitude. If the string does represent a durationdelta object, it will return None. """ - match = _duration_parser.fullmatch(duration) + match = _DURATION_REGEX.fullmatch(duration) if not match: return None -- cgit v1.2.3 From 6dbf8ded81716f2bf55ca4d6297e3154afcdd285 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Fri, 26 Feb 2021 19:07:47 +0200 Subject: Sync alert won't trigger with fake redis The alert will trigger with fake redis on every bot startup even when people aren't working on the defcon cog. Added a condition to check if fake redis is being used. --- bot/exts/moderation/defcon.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index a88892b13..aa6dc0790 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -14,7 +14,7 @@ from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot -from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles +from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Redis, Roles from bot.converters import DurationDelta, Expiry from bot.exts.moderation.modlog import ModLog from bot.utils.messages import format_user @@ -89,10 +89,11 @@ class Defcon(Cog): self.expiry = datetime.fromisoformat(settings["expiry"]) if settings["expiry"] else None except Exception: log.exception("Unable to get DEFCON settings!") - await self.channel.send( - f"<@&{Roles.moderators}> <@&{Roles.devops}> **WARNING**: Unable to get DEFCON settings!" - f"\n\n```{traceback.format_exc()}```" - ) + if not Redis.use_fakeredis: + await self.channel.send( + f"<@&{Roles.moderators}> <@&{Roles.devops}> **WARNING**: Unable to get DEFCON settings!" + f"\n\n```{traceback.format_exc()}```" + ) else: if self.expiry: -- cgit v1.2.3 From 64e85ddcc57e2789627c4a4a7869424d7583dc17 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Fri, 26 Feb 2021 21:02:31 +0000 Subject: !int socketstats improvements - Comma separate event values - Make fields inline for smaller embed --- bot/exts/utils/internal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/internal.py b/bot/exts/utils/internal.py index a7ab43f37..d193e4d4f 100644 --- a/bot/exts/utils/internal.py +++ b/bot/exts/utils/internal.py @@ -245,7 +245,7 @@ async def func(): # (None,) -> Any ) for event_type, count in self.socket_events.most_common(25): - stats_embed.add_field(name=event_type, value=count, inline=False) + stats_embed.add_field(name=event_type, value=f"{count:,}", inline=True) await ctx.send(embed=stats_embed) -- cgit v1.2.3 From de226ea845e8f68735ce6d20193bece9f50b1d5f Mon Sep 17 00:00:00 2001 From: Gustav Odinger <65498475+gustavwilliam@users.noreply.github.com> Date: Fri, 26 Feb 2021 22:24:50 +0100 Subject: Make "event" plural in socketstats embed --- bot/exts/utils/internal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/utils/internal.py b/bot/exts/utils/internal.py index d193e4d4f..6f2da3131 100644 --- a/bot/exts/utils/internal.py +++ b/bot/exts/utils/internal.py @@ -240,7 +240,7 @@ async def func(): # (None,) -> Any stats_embed = discord.Embed( title="WebSocket statistics", - description=f"Receiving {per_s:0.2f} event per second.", + description=f"Receiving {per_s:0.2f} events per second.", color=discord.Color.blurple() ) -- cgit v1.2.3 From e82429c88f8643f8eaa89ea5541d0ffe860ec338 Mon Sep 17 00:00:00 2001 From: SavagePastaMan <69145546+SavagePastaMan@users.noreply.github.com> Date: Sat, 27 Feb 2021 09:54:50 -0500 Subject: Create comparison.md --- bot/resources/tags/comparison.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 bot/resources/tags/comparison.md diff --git a/bot/resources/tags/comparison.md b/bot/resources/tags/comparison.md new file mode 100644 index 000000000..12844bd2f --- /dev/null +++ b/bot/resources/tags/comparison.md @@ -0,0 +1,12 @@ +**Assignment vs. Comparison** + +The assignment operator (`=`) is used to assign variables. +```python +x = 5 +print(x) # Prints 5 +``` +The equality operator (`==`) is used to compare values. +```python +if x == 5: + print("The value of x is 5") +``` -- cgit v1.2.3 From c3a9e704080bfc670993b396d721ffd762348591 Mon Sep 17 00:00:00 2001 From: Bast Date: Mon, 1 Mar 2021 02:20:53 -0800 Subject: Add alias !u for !user --- bot/exts/info/information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 4499e4c25..88e904d03 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -202,7 +202,7 @@ class Information(Cog): await ctx.send(embed=embed) - @command(name="user", aliases=["user_info", "member", "member_info"]) + @command(name="user", aliases=["user_info", "member", "member_info", "u"]) async def user_info(self, ctx: Context, user: FetchedMember = None) -> None: """Returns info about a user.""" if user is None: -- cgit v1.2.3 From 5b31aa992db27cd1798e4dce5f1c4256aa8848fa Mon Sep 17 00:00:00 2001 From: Bast Date: Mon, 1 Mar 2021 02:22:59 -0800 Subject: Add alias !tban for !tempban --- bot/exts/moderation/infraction/infractions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 7349d65f2..406c6b53f 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -126,7 +126,7 @@ class Infractions(InfractionScheduler, commands.Cog): duration = await Duration().convert(ctx, "1h") await self.apply_mute(ctx, user, reason, expires_at=duration) - @command() + @command(aliases=("tban",)) async def tempban( self, ctx: Context, -- cgit v1.2.3 From 58c37361d0a322b308869492d50f2008ae497b3d Mon Sep 17 00:00:00 2001 From: Bast Date: Mon, 1 Mar 2021 02:23:46 -0800 Subject: Add !superstar and !unsuperstar aliases for !superstarify --- bot/exts/moderation/infraction/superstarify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index ffc470c54..704dddf9c 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -104,7 +104,7 @@ class Superstarify(InfractionScheduler, Cog): await self.reapply_infraction(infraction, action) - @command(name="superstarify", aliases=("force_nick", "star", "starify")) + @command(name="superstarify", aliases=("force_nick", "star", "starify", "superstar")) async def superstarify( self, ctx: Context, @@ -183,7 +183,7 @@ class Superstarify(InfractionScheduler, Cog): ) await ctx.send(embed=embed) - @command(name="unsuperstarify", aliases=("release_nick", "unstar", "unstarify")) + @command(name="unsuperstarify", aliases=("release_nick", "unstar", "unstarify", "unsuperstar")) async def unsuperstarify(self, ctx: Context, member: Member) -> None: """Remove the superstarify infraction and allow the user to change their nickname.""" await self.pardon_infraction(ctx, "superstar", member) -- cgit v1.2.3 From dec9a9dba77aa4322f9dc37b6493a8410e7482ec Mon Sep 17 00:00:00 2001 From: Bast Date: Mon, 1 Mar 2021 02:38:41 -0800 Subject: Add !stban alias for !shadowtempban --- bot/exts/moderation/infraction/infractions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 406c6b53f..3b5b1df45 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -198,7 +198,7 @@ class Infractions(InfractionScheduler, commands.Cog): # endregion # region: Temporary shadow infractions - @command(hidden=True, aliases=["shadowtempban", "stempban"]) + @command(hidden=True, aliases=["shadowtempban", "stempban", "stban"]) async def shadow_tempban( self, ctx: Context, -- cgit v1.2.3 From 2293b9dc78d21a80043a9e9d24b9442caf7579df Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Mon, 1 Mar 2021 19:46:08 +0200 Subject: Change to handle specifically redis errors The idea to ignore alerts on fake redis didn't solve the problem completely, because sometimes you'll just develop with a real redis. It also didn't solve the ping we would get on first start up. After looking into it there seems like there's no actual reason to alert on key errors, as they should only happen if the cache gets wiped for some reason, which shouldn't happen, but in which case we have bigger issues. Alerts are therefore limited to connection errors raised by redis. This additionally handles only redis errors when writing to it as well. If any other error is raised it is ok for the function to stop at that point, as all variables have already been set. The only thing which doesn't get executed is the confirmation message and logging, the lack of which is an exception message in itself. --- bot/exts/moderation/defcon.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index aa6dc0790..3d3f0e81e 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -2,11 +2,11 @@ import asyncio import logging import traceback from collections import namedtuple -from contextlib import suppress from datetime import datetime from enum import Enum from typing import Optional, Union +from aioredis import RedisError from async_rediscache import RedisCache from dateutil.relativedelta import relativedelta from discord import Colour, Embed, Member, User @@ -14,7 +14,7 @@ from discord.ext import tasks from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot -from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Redis, Roles +from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles from bot.converters import DurationDelta, Expiry from bot.exts.moderation.modlog import ModLog from bot.utils.messages import format_user @@ -87,13 +87,12 @@ class Defcon(Cog): settings = await self.defcon_settings.to_dict() self.threshold = parse_duration_string(settings["threshold"]) if settings["threshold"] else None self.expiry = datetime.fromisoformat(settings["expiry"]) if settings["expiry"] else None - except Exception: + except RedisError: log.exception("Unable to get DEFCON settings!") - if not Redis.use_fakeredis: - await self.channel.send( - f"<@&{Roles.moderators}> <@&{Roles.devops}> **WARNING**: Unable to get DEFCON settings!" - f"\n\n```{traceback.format_exc()}```" - ) + await self.channel.send( + f"<@&{Roles.moderators}> <@&{Roles.devops}> **WARNING**: Unable to get DEFCON settings!" + f"\n\n```{traceback.format_exc()}```" + ) else: if self.expiry: @@ -210,14 +209,19 @@ class Defcon(Cog): if self.expiry is not None: self.scheduler.schedule_at(expiry, 0, self._remove_threshold()) - with suppress(Exception): + self._update_notifier() + + # Make sure to handle the critical part of the update before writing to Redis. + error = "" + try: await self.defcon_settings.update( { 'threshold': Defcon._stringify_relativedelta(self.threshold) if self.threshold else "", 'expiry': expiry.isoformat() if expiry else 0 } ) - self._update_notifier() + except RedisError: + error = ", but failed to write to cache" action = Action.DURATION_UPDATE @@ -234,7 +238,7 @@ class Defcon(Cog): channel_message = "removed" await self.channel.send( - f"{action.value.emoji} DEFCON threshold {channel_message}." + f"{action.value.emoji} DEFCON threshold {channel_message}{error}." ) await self._send_defcon_log(action, author) self._update_channel_topic() -- cgit v1.2.3 From b9d1de268fdaa67413e1ac4f24057cd6ecc9771d Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Mon, 1 Mar 2021 19:50:54 +0200 Subject: Provide default cache values when syncing --- bot/exts/moderation/defcon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 3d3f0e81e..482ebe13b 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -85,8 +85,8 @@ class Defcon(Cog): try: settings = await self.defcon_settings.to_dict() - self.threshold = parse_duration_string(settings["threshold"]) if settings["threshold"] else None - self.expiry = datetime.fromisoformat(settings["expiry"]) if settings["expiry"] else None + self.threshold = parse_duration_string(settings["threshold"]) if settings.get("threshold") else None + self.expiry = datetime.fromisoformat(settings["expiry"]) if settings.get("expiry") else None except RedisError: log.exception("Unable to get DEFCON settings!") await self.channel.send( -- cgit v1.2.3 From ca3389eb2e2a796c5f757b37e5e2fa6f308c4dbf Mon Sep 17 00:00:00 2001 From: mbaruh Date: Tue, 2 Mar 2021 20:45:20 +0200 Subject: Improved docstring for threshold command. --- bot/exts/moderation/defcon.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 86dece518..02302612f 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -161,7 +161,14 @@ class Defcon(Cog): async def threshold( self, ctx: Context, threshold: Union[DurationDelta, int], expiry: Optional[Expiry] = None ) -> None: - """Set how old an account must be to join the server.""" + """ + Set how old an account must be to join the server. + + The threshold is the minimum required account age. Can accept either a duration string or a number of days. + Set it to 0 to have no threshold. + The expiry allows to automatically remove the threshold after a designated time. If no expiry is specified, + the cog will remind to remove the threshold hourly. + """ if isinstance(threshold, int): threshold = relativedelta(days=threshold) await self._update_threshold(ctx.author, threshold=threshold, expiry=expiry) -- cgit v1.2.3 From e9bdccf7d51107691fab2f25573241a2e524d32a Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 6 Mar 2021 11:59:22 +0000 Subject: Add JSON logging dependencies --- Pipfile | 1 + Pipfile.lock | 260 +++++++++++++++++++++++++++++++++++++---------------------- 2 files changed, 163 insertions(+), 98 deletions(-) diff --git a/Pipfile b/Pipfile index 0a94fb888..f92d9ea68 100644 --- a/Pipfile +++ b/Pipfile @@ -28,6 +28,7 @@ sphinx = "~=2.2" statsd = "~=3.3" arrow = "~=0.17" emoji = "~=0.6" +python-json-logger = "*" [dev-packages] coverage = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index f8cedb08f..f19f91ce8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "228ae55fe5700ac3827ba6b661933b60b1d06f44fea8bcbe8c5a769fa10ab2fd" + "sha256": "a55fa167f9581360b1258a6564ab9ae68f9c56dd9e7997f9c5a2f102be28c69c" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "aio-pika": { "hashes": [ - "sha256:9773440a89840941ac3099a7720bf9d51e8764a484066b82ede4d395660ff430", - "sha256:a8065be3c722eb8f9fff8c0e7590729e7782202cdb9363d9830d7d5d47b45c7c" + "sha256:1d4305a5f78af3857310b4fe48348cdcf6c097e0e275ea88c2cd08570531a369", + "sha256:e69afef8695f47c5d107bbdba21bdb845d5c249acb3be53ef5c2d497b02657c0" ], "index": "pypi", - "version": "==6.7.1" + "version": "==6.8.0" }, "aiodns": { "hashes": [ @@ -96,6 +96,7 @@ "sha256:8218dd9f7198d6e7935855468326bbacf0089f926c70baa8dd92944cb2496573", "sha256:e584dac13a242589aaf42470fd3006cb0dc5aed6506cbd20357c7ec8bbe4a89e" ], + "markers": "python_version >= '3.6'", "version": "==3.3.1" }, "alabaster": { @@ -122,6 +123,7 @@ "sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af" ], "index": "pypi", + "markers": "python_version ~= '3.7'", "version": "==0.1.4" }, "async-timeout": { @@ -129,6 +131,7 @@ "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" ], + "markers": "python_full_version >= '3.5.3'", "version": "==3.0.1" }, "attrs": { @@ -136,6 +139,7 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "babel": { @@ -143,6 +147,7 @@ "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0" }, "beautifulsoup4": { @@ -215,7 +220,6 @@ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "index": "pypi", "markers": "sys_platform == 'win32'", "version": "==0.4.4" }, @@ -248,6 +252,7 @@ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, "emoji": { @@ -330,6 +335,7 @@ "sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390", "sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "humanfriendly": { @@ -337,6 +343,7 @@ "sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d", "sha256:d5c731705114b9ad673754f3317d9fa4c23212f36b29bdc4272a892eafc9bc72" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==9.1" }, "idna": { @@ -344,6 +351,7 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "imagesize": { @@ -351,6 +359,7 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, "jinja2": { @@ -358,6 +367,7 @@ "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.3" }, "lxml": { @@ -466,15 +476,16 @@ "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "more-itertools": { "hashes": [ - "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330", - "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf" + "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", + "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" ], "index": "pypi", - "version": "==8.6.0" + "version": "==8.7.0" }, "multidict": { "hashes": [ @@ -516,12 +527,14 @@ "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" ], + "markers": "python_version >= '3.6'", "version": "==5.1.0" }, "ordered-set": { "hashes": [ "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95" ], + "markers": "python_version >= '3.5'", "version": "==4.0.2" }, "packaging": { @@ -529,6 +542,7 @@ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.9" }, "pamqp": { @@ -577,6 +591,7 @@ "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.20" }, "pygments": { @@ -584,6 +599,7 @@ "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" ], + "markers": "python_version >= '3.5'", "version": "==2.8.0" }, "pyparsing": { @@ -591,6 +607,7 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "python-dateutil": { @@ -601,6 +618,13 @@ "index": "pypi", "version": "==2.8.1" }, + "python-json-logger": { + "hashes": [ + "sha256:f26eea7898db40609563bed0a7ca11af12e2a79858632706d835a0f961b7d398" + ], + "index": "pypi", + "version": "==2.0.1" + }, "pytz": { "hashes": [ "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", @@ -610,28 +634,37 @@ }, "pyyaml": { "hashes": [ - "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", - "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", - "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", - "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", - "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", - "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", - "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", - "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", - "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", - "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", - "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", - "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", - "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc" ], "index": "pypi", - "version": "==5.3.1" + "version": "==5.4.1" }, "redis": { "hashes": [ "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2", "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.5.3" }, "requests": { @@ -644,17 +677,18 @@ }, "sentry-sdk": { "hashes": [ - "sha256:0a711ec952441c2ec89b8f5d226c33bc697914f46e876b44a4edd3e7864cf4d0", - "sha256:737a094e49a529dd0fdcaafa9e97cf7c3d5eb964bd229821d640bc77f3502b3f" + "sha256:4ae8d1ced6c67f1c8ea51d82a16721c166c489b76876c9f2c202b8a50334b237", + "sha256:e75c8c58932bda8cd293ea8e4b242527129e1caaec91433d21b8b2f20fee030b" ], "index": "pypi", - "version": "==0.19.5" + "version": "==0.20.3" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -692,6 +726,7 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -699,6 +734,7 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], + "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -706,6 +742,7 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -713,6 +750,7 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], + "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -720,6 +758,7 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], + "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -727,6 +766,7 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], + "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "statsd": { @@ -750,6 +790,7 @@ "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.3" }, "yarl": { @@ -792,6 +833,7 @@ "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], + "markers": "python_version >= '3.6'", "version": "==1.6.3" } }, @@ -808,6 +850,7 @@ "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.3.0" }, "certifi": { @@ -822,6 +865,7 @@ "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d", "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1" ], + "markers": "python_full_version >= '3.6.1'", "version": "==3.2.0" }, "chardet": { @@ -833,58 +877,61 @@ }, "coverage": { "hashes": [ - "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297", - "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1", - "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497", - "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606", - "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528", - "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b", - "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4", - "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830", - "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1", - "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f", - "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d", - "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3", - "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8", - "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500", - "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7", - "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb", - "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b", - "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059", - "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b", - "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72", - "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36", - "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277", - "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c", - "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631", - "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff", - "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8", - "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec", - "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b", - "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7", - "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105", - "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b", - "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c", - "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b", - "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98", - "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4", - "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879", - "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f", - "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4", - "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044", - "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e", - "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899", - "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f", - "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448", - "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714", - "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2", - "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d", - "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd", - "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7", - "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae" + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" ], "index": "pypi", - "version": "==5.3.1" + "version": "==5.5" }, "coveralls": { "hashes": [ @@ -924,11 +971,11 @@ }, "flake8-annotations": { "hashes": [ - "sha256:3a377140556aecf11fa9f3bb18c10db01f5ea56dc79a730e2ec9b4f1f49e2055", - "sha256:e17947a48a5b9f632fe0c72682fc797c385e451048e7dfb20139f448a074cb3e" + "sha256:8968ff12f296433028ad561c680ccc03a7cd62576d100c3f1475e058b3c11b43", + "sha256:bd0505616c0d85ebb45c6052d339c69f320d3f87fa079ab4e91a4f234a863d05" ], "index": "pypi", - "version": "==2.5.0" + "version": "==2.6.0" }, "flake8-bugbear": { "hashes": [ @@ -986,16 +1033,18 @@ }, "identify": { "hashes": [ - "sha256:de7129142a5c86d75a52b96f394d94d96d497881d2aaf8eafe320cdbe8ac4bcc", - "sha256:e0dae57c0397629ce13c289f6ddde0204edf518f557bfdb1e56474aa143e77c3" + "sha256:2179e7359471ab55729f201b3fdf7dc2778e221f868410fedcb0987b791ba552", + "sha256:2a5fdf2f5319cc357eda2550bea713a404392495961022cf2462624ce62f0f46" ], - "version": "==1.5.14" + "markers": "python_full_version >= '3.6.1'", + "version": "==2.1.0" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "mccabe": { @@ -1022,17 +1071,18 @@ }, "pre-commit": { "hashes": [ - "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0", - "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4" + "sha256:16212d1fde2bed88159287da88ff03796863854b04dc9f838a55979325a3d20e", + "sha256:399baf78f13f4de82a29b649afd74bef2c4e28eb4f021661fc7f29246e8c7a3a" ], "index": "pypi", - "version": "==2.9.3" + "version": "==2.10.1" }, "pycodestyle": { "hashes": [ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pydocstyle": { @@ -1040,6 +1090,7 @@ "sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325", "sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678" ], + "markers": "python_version >= '3.5'", "version": "==5.1.1" }, "pyflakes": { @@ -1047,26 +1098,35 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pyyaml": { "hashes": [ - "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", - "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", - "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", - "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e", - "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", - "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", - "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", - "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", - "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", - "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a", - "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", - "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", - "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc" ], "index": "pypi", - "version": "==5.3.1" + "version": "==5.4.1" }, "requests": { "hashes": [ @@ -1081,6 +1141,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "snowballstemmer": { @@ -1095,6 +1156,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "urllib3": { @@ -1102,6 +1164,7 @@ "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.3" }, "virtualenv": { @@ -1109,6 +1172,7 @@ "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d", "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.4.2" } } -- cgit v1.2.3 From fd7a693a4ed4268ab4823142c21e9c85973a3d4f Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 6 Mar 2021 12:08:07 +0000 Subject: Use JSON logging when debug mode is disabled --- bot/log.py | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/bot/log.py b/bot/log.py index e92233a33..bc3bba0af 100644 --- a/bot/log.py +++ b/bot/log.py @@ -1,11 +1,12 @@ import logging import os import sys -from logging import Logger, handlers +from logging import Logger, StreamHandler, handlers from pathlib import Path import coloredlogs import sentry_sdk +from pythonjsonlogger import jsonlogger from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.redis import RedisIntegration @@ -13,6 +14,15 @@ from bot import constants TRACE_LEVEL = 5 +PROD_FIELDS = [ + "asctime", + "name", + "levelname", + "message", + "funcName", + "filename" +] + def setup() -> None: """Set up loggers.""" @@ -33,21 +43,28 @@ def setup() -> None: root_log.setLevel(log_level) root_log.addHandler(file_handler) - if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: - coloredlogs.DEFAULT_LEVEL_STYLES = { - **coloredlogs.DEFAULT_LEVEL_STYLES, - "trace": {"color": 246}, - "critical": {"background": "red"}, - "debug": coloredlogs.DEFAULT_LEVEL_STYLES["info"] - } - - if "COLOREDLOGS_LOG_FORMAT" not in os.environ: - coloredlogs.DEFAULT_LOG_FORMAT = format_string - - if "COLOREDLOGS_LOG_LEVEL" not in os.environ: - coloredlogs.DEFAULT_LOG_LEVEL = log_level - - coloredlogs.install(logger=root_log, stream=sys.stdout) + if constants.DEBUG_MODE: + if "COLOREDLOGS_LEVEL_STYLES" not in os.environ: + coloredlogs.DEFAULT_LEVEL_STYLES = { + **coloredlogs.DEFAULT_LEVEL_STYLES, + "trace": {"color": 246}, + "critical": {"background": "red"}, + "debug": coloredlogs.DEFAULT_LEVEL_STYLES["info"] + } + + if "COLOREDLOGS_LOG_FORMAT" not in os.environ: + coloredlogs.DEFAULT_LOG_FORMAT = format_string + + if "COLOREDLOGS_LOG_LEVEL" not in os.environ: + coloredlogs.DEFAULT_LOG_LEVEL = log_level + + coloredlogs.install(logger=root_log, stream=sys.stdout) + else: + json_format = " ".join([f"%({field})s" for field in PROD_FIELDS]) + stream_handler = StreamHandler() + formatter = jsonlogger.JsonFormatter(json_format) + stream_handler.setFormatter(formatter) + root_log.addHandler(stream_handler) logging.getLogger("discord").setLevel(logging.WARNING) logging.getLogger("websockets").setLevel(logging.WARNING) -- cgit v1.2.3 From 477956d44b331e57949d860eb3bfe985007a3b18 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sat, 6 Mar 2021 13:13:39 +0000 Subject: Version lock JSON logger --- Pipfile | 2 +- Pipfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index f92d9ea68..024aa6eff 100644 --- a/Pipfile +++ b/Pipfile @@ -28,7 +28,7 @@ sphinx = "~=2.2" statsd = "~=3.3" arrow = "~=0.17" emoji = "~=0.6" -python-json-logger = "*" +python-json-logger = "~=2.0" [dev-packages] coverage = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index f19f91ce8..dc7f6f21f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a55fa167f9581360b1258a6564ab9ae68f9c56dd9e7997f9c5a2f102be28c69c" + "sha256": "81ca9d1891e71de1c3f71958f082e1a8cad71e5b3ca425dc561d0ae74664fdb0" }, "pipfile-spec": 6, "requires": { -- cgit v1.2.3 From a98ecbfd2e3446ca9a17566220c41235d1328fcb Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 6 Mar 2021 17:44:34 +0200 Subject: Filtering hotfix Bug caused by an outdated function signature in a previous commit in the #1402 PR --- bot/exts/filters/filtering.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 4093ba4ad..946bbf2c3 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -240,7 +240,13 @@ class Filtering(Cog): # We also do not need to worry about filters that take the full message, # since all we have is an arbitrary string. if _filter["enabled"] and _filter["content_only"]: - match, reason = await _filter["function"](result) + filter_result = await _filter["function"](result) + reason = None + + if isinstance(filter_result, tuple): + match, reason = filter_result + else: + match = filter_result if match: # If this is a filter (not a watchlist), we set the variable so we know -- cgit v1.2.3 From 8f1ef21bc2e140d39dabb16de8fbbd6077805a25 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 6 Mar 2021 17:47:16 +0200 Subject: Remove trailing whitespace --- bot/exts/filters/filtering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 946bbf2c3..c90b18dcb 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -242,7 +242,7 @@ class Filtering(Cog): if _filter["enabled"] and _filter["content_only"]: filter_result = await _filter["function"](result) reason = None - + if isinstance(filter_result, tuple): match, reason = filter_result else: -- cgit v1.2.3 From fa016c096ef249ea1b8d722633882a09535e9c44 Mon Sep 17 00:00:00 2001 From: xithrius Date: Thu, 11 Feb 2021 01:51:40 -0800 Subject: Added filter. --- bot/exts/info/pypi.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index 3e326e8bb..8fe249c8a 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -1,6 +1,7 @@ import itertools import logging import random +import re from discord import Embed from discord.ext.commands import Cog, Context, command @@ -12,8 +13,11 @@ from bot.constants import Colours, NEGATIVE_REPLIES URL = "https://pypi.org/pypi/{package}/json" FIELDS = ("author", "requires_python", "summary", "license") PYPI_ICON = "https://cdn.discordapp.com/emojis/766274397257334814.png" + PYPI_COLOURS = itertools.cycle((Colours.yellow, Colours.blue, Colours.white)) +ILLEGAL_CHARACTERS = re.compile(r"[^a-zA-Z0-9-.]+") + log = logging.getLogger(__name__) @@ -32,6 +36,11 @@ class PyPi(Cog): ) embed.set_thumbnail(url=PYPI_ICON) + if (character := re.search(ILLEGAL_CHARACTERS, package)) is not None: + embed.description = f"Illegal character passed into command: '{escape_markdown(character.group(0))}'" + await ctx.send(embed=embed) + return + async with self.bot.http_session.get(URL.format(package=package)) as response: if response.status == 404: embed.description = "Package could not be found." -- cgit v1.2.3 From cd71b8447eaad67b6885d99e00c230198c21cf0e Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Sun, 7 Mar 2021 16:27:17 +0100 Subject: Mark #appeals as a mod channel --- config-default.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config-default.yml b/config-default.yml index 18d9cd370..3dbc7bd6b 100644 --- a/config-default.yml +++ b/config-default.yml @@ -195,6 +195,7 @@ guild: incidents_archive: 720668923636351037 mods: &MODS 305126844661760000 mod_alerts: 473092532147060736 + mod_appeals: &MOD_APPEALS 808790025688711198 mod_meta: &MOD_META 775412552795947058 mod_spam: &MOD_SPAM 620607373828030464 mod_tools: &MOD_TOOLS 775413915391098921 @@ -230,6 +231,7 @@ guild: moderation_channels: - *ADMINS - *ADMIN_SPAM + - *MOD_APPEALS - *MOD_META - *MOD_TOOLS - *MODS -- cgit v1.2.3