From 1ec0a10811c26351e823c29637e16837a761e372 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 23 Feb 2020 16:53:35 -0800 Subject: Resources: add JSON with array of chemical element names --- bot/resources/elements.json | 120 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 bot/resources/elements.json diff --git a/bot/resources/elements.json b/bot/resources/elements.json new file mode 100644 index 000000000..61be9105f --- /dev/null +++ b/bot/resources/elements.json @@ -0,0 +1,120 @@ +[ + "Hydrogen", + "Helium", + "Lithium", + "Beryllium", + "Boron", + "Carbon", + "Nitrogen", + "Oxygen", + "Fluorine", + "Neon", + "Sodium", + "Magnesium", + "Aluminium", + "Silicon", + "Phosphorus", + "Sulfur", + "Chlorine", + "Argon", + "Potassium", + "Calcium", + "Scandium", + "Titanium", + "Vanadium", + "Chromium", + "Manganese", + "Iron", + "Cobalt", + "Nickel", + "Copper", + "Zinc", + "Gallium", + "Germanium", + "Arsenic", + "Selenium", + "Bromine", + "Krypton", + "Rubidium", + "Strontium", + "Yttrium", + "Zirconium", + "Niobium", + "Molybdenum", + "Technetium", + "Ruthenium", + "Rhodium", + "Palladium", + "Silver", + "Cadmium", + "Indium", + "Tin", + "Antimony", + "Tellurium", + "Iodine", + "Xenon", + "Caesium", + "Barium", + "Lanthanum", + "Cerium", + "Praseodymium", + "Neodymium", + "Promethium", + "Samarium", + "Europium", + "Gadolinium", + "Terbium", + "Dysprosium", + "Holmium", + "Erbium", + "Thulium", + "Ytterbium", + "Lutetium", + "Hafnium", + "Tantalum", + "Tungsten", + "Rhenium", + "Osmium", + "Iridium", + "Platinum", + "Gold", + "Mercury", + "Thallium", + "Lead", + "Bismuth", + "Polonium", + "Astatine", + "Radon", + "Francium", + "Radium", + "Actinium", + "Thorium", + "Protactinium", + "Uranium", + "Neptunium", + "Plutonium", + "Americium", + "Curium", + "Berkelium", + "Californium", + "Einsteinium", + "Fermium", + "Mendelevium", + "Nobelium", + "Lawrencium", + "Rutherfordium", + "Dubnium", + "Seaborgium", + "Bohrium", + "Hassium", + "Meitnerium", + "Darmstadtium", + "Roentgenium", + "Copernicium", + "Nihonium", + "Flerovium", + "Moscovium", + "Livermorium", + "Tennessine", + "Oganesson" +] -- cgit v1.2.3 From 3e5bd7328dc3e0d7ae25bb26e087294e4288afe6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 23 Feb 2020 16:58:25 -0800 Subject: HelpChannels: create boilerplate extension and cog --- bot/cogs/help_channels.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 bot/cogs/help_channels.py diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py new file mode 100644 index 000000000..e4febdcaa --- /dev/null +++ b/bot/cogs/help_channels.py @@ -0,0 +1,12 @@ +from discord.ext import commands + +from bot.bot import Bot + + +class HelpChannels(commands.Cog): + """Manage the help channel system of the guild.""" + + +def setup(bot: Bot) -> None: + """Load the HelpChannels cog.""" + bot.add_cog(HelpChannels(bot)) -- cgit v1.2.3 From 439c0dddaecec3da3c804dffda14342ed3ce055d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 23 Feb 2020 17:00:49 -0800 Subject: HelpChannels: load element names from JSON --- bot/cogs/help_channels.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index e4febdcaa..561c4d2c9 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,8 +1,15 @@ +import json +from pathlib import Path + from discord.ext import commands from bot.bot import Bot +with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: + ELEMENTS = json.load(elements_file) + + class HelpChannels(commands.Cog): """Manage the help channel system of the guild.""" -- cgit v1.2.3 From 01bf328bea69dfa773d42aab2fb43dcb2b218e0c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 23 Feb 2020 17:22:28 -0800 Subject: Constants: add constants for HelpChannels cog --- bot/constants.py | 8 ++++++++ config-default.yml | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 14f8dc094..4b47db03d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -532,6 +532,14 @@ class Free(metaclass=YAMLGetter): cooldown_per: float +class HelpChannels(metaclass=YAMLGetter): + section = 'help_channels' + + cmd_whitelist: List[int] + idle_minutes: int + max_available: int + + class Mention(metaclass=YAMLGetter): section = 'mention' diff --git a/config-default.yml b/config-default.yml index 5788d1e12..1f2b12412 100644 --- a/config-default.yml +++ b/config-default.yml @@ -512,6 +512,17 @@ mention: message_timeout: 300 reset_delay: 5 +help_channels: + # Roles which are allowed to use the command which makes channels dormant + cmd_whitelist: + - *HELPERS_ROLE + + # Allowed duration of inactivity before making a channel dormant + idle_minutes: 45 + + # Maximum number of channels to put in the available category + max_available: 2 + redirect_output: delete_invocation: true delete_delay: 15 -- cgit v1.2.3 From 73d218b8bebde2016f0cff215e43e53d8e781608 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 23 Feb 2020 17:32:58 -0800 Subject: HelpChannels: add constants for active/dormant messages --- bot/cogs/help_channels.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 561c4d2c9..6bcaaf624 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -3,8 +3,32 @@ from pathlib import Path from discord.ext import commands +from bot import constants from bot.bot import Bot +ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" + +AVAILABLE_MSG = f""" +This help channel is now **available**, which means that you can claim it by simply typing your \ +question into it. Once claimed, the channel will move into the **Help: In Use** category, and will \ +be yours until it has been inactive for {constants.HelpChannels.idle_minutes}. When that happens, \ +it will be set to **dormant** and moved into the **Help: Dormant** category. + +Try to write the best question you can by providing a detailed description and telling us what \ +you've tried already. For more information on asking a good question, \ +[check out our guide on asking good questions]({ASKING_GUIDE_URL}). +""" + +DORMANT_MSG = f""" +This help channel has been marked as **dormant**, and has been moved into the **Help: Dormant** \ +category at the bottom of the channel list. It is no longer possible to send messages in this \ +channel until it becomes available again. + +If your question wasn't answered yet, you can claim a new help channel from the \ +**Help: Available** category by simply asking your question again. Consider rephrasing the \ +question to maximize your chance of getting a good answer. If you're not sure how, have a look \ +through [our guide for asking a good question]({ASKING_GUIDE_URL}). +""" with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: ELEMENTS = json.load(elements_file) -- cgit v1.2.3 From db185326bace6eb249fc2867472ae7d770f249db Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 23 Feb 2020 18:17:02 -0800 Subject: Constants: add help category constants The original category was re-purposed as the "in-use" category so that deployment of the new system will not interrupt ongoing help sessions. --- bot/cogs/free.py | 2 +- bot/constants.py | 4 +++- config-default.yml | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/cogs/free.py b/bot/cogs/free.py index 33b55e79a..99516fade 100644 --- a/bot/cogs/free.py +++ b/bot/cogs/free.py @@ -19,7 +19,7 @@ PER = Free.cooldown_per class Free(Cog): """Tries to figure out which help channels are free.""" - PYTHON_HELP_ID = Categories.python_help + PYTHON_HELP_ID = Categories.help_in_use @command(name="free", aliases=('f',)) @redirect_output(destination_channel=Channels.bot_commands, bypass_roles=STAFF_ROLES) diff --git a/bot/constants.py b/bot/constants.py index 4b47db03d..58a236546 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -356,7 +356,9 @@ class Categories(metaclass=YAMLGetter): section = "guild" subsection = "categories" - python_help: int + help_available: int + help_in_use: int + help_dormant: int class Channels(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index 1f2b12412..27143ff30 100644 --- a/config-default.yml +++ b/config-default.yml @@ -111,7 +111,9 @@ guild: id: 267624335836053506 categories: - python_help: 356013061213126657 + help_available: 691405807388196926 + help_in_use: 356013061213126657 + help_dormant: 691405908919451718 channels: announcements: 354619224620138496 -- cgit v1.2.3 From de9193d2685ebc4e1cf081003b278ef2ad3cee13 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 11:41:51 -0800 Subject: HelpChannels: add method stubs --- bot/cogs/help_channels.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 6bcaaf624..eeb3f3684 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,10 +1,14 @@ +import asyncio import json +from collections import deque from pathlib import Path +import discord from discord.ext import commands from bot import constants from bot.bot import Bot +from bot.utils.scheduling import Scheduler ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" @@ -34,9 +38,52 @@ with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file ELEMENTS = json.load(elements_file) -class HelpChannels(commands.Cog): +class HelpChannels(Scheduler, commands.Cog): """Manage the help channel system of the guild.""" + def __init__(self, bot: Bot): + super().__init__() + + self.bot = bot + + async def create_channel_queue(self) -> asyncio.Queue: + """Return a queue of dormant channels to use for getting the next available channel.""" + + async def create_dormant(self) -> discord.TextChannel: + """Create and return a new channel in the Dormant category.""" + + async def create_name_queue(self) -> deque: + """Return a queue of element names to use for creating new channels.""" + + @commands.command(name="dormant") + async def dormant_command(self) -> None: + """Make the current in-use help channel dormant.""" + + async def get_available_candidate(self) -> discord.TextChannel: + """Return a dormant channel to turn into an available channel.""" + + async def get_idle_time(self, channel: discord.TextChannel) -> int: + """Return the time elapsed since the last message sent in the `channel`.""" + + async def init_available(self) -> None: + """Initialise the Available category with channels.""" + + async def move_idle_channels(self) -> None: + """Make all idle in-use channels dormant.""" + + async def move_to_available(self) -> None: + """Make a channel available.""" + + async def move_to_dormant(self, channel: discord.TextChannel) -> None: + """Make the `channel` dormant.""" + + @commands.Cog.listener() + async def on_message(self, message: discord.Message) -> None: + """Move an available channel to the In Use category and replace it with a dormant one.""" + + async def _scheduled_task(self, channel: discord.TextChannel, timeout: int) -> None: + """Make the `channel` dormant after `timeout` seconds or reschedule if it's still active.""" + def setup(bot: Bot) -> None: """Load the HelpChannels cog.""" -- cgit v1.2.3 From c4abc45ee21e0070b38f89c4417efd0c0982ea31 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 11:58:28 -0800 Subject: HelpChannels: add a logger --- bot/cogs/help_channels.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index eeb3f3684..a75314f62 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,5 +1,6 @@ import asyncio import json +import logging from collections import deque from pathlib import Path @@ -10,6 +11,8 @@ from bot import constants from bot.bot import Bot from bot.utils.scheduling import Scheduler +log = logging.getLogger(__name__) + ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" AVAILABLE_MSG = f""" -- cgit v1.2.3 From 61be5a13eb6c93dc689cc0dad13206d139c8ad89 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 12:10:37 -0800 Subject: HelpChannels: add a function to get a channel or fetch it from API --- bot/cogs/help_channels.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index a75314f62..5f5129149 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -84,6 +84,14 @@ class HelpChannels(Scheduler, commands.Cog): async def on_message(self, message: discord.Message) -> None: """Move an available channel to the In Use category and replace it with a dormant one.""" + async def try_get_channel(self, channel_id: int) -> discord.abc.GuildChannel: + """Attempt to get or fetch a channel and return it.""" + channel = self.bot.get_channel(channel_id) + if not channel: + channel = await self.bot.fetch_channel(channel_id) + + return channel + async def _scheduled_task(self, channel: discord.TextChannel, timeout: int) -> None: """Make the `channel` dormant after `timeout` seconds or reschedule if it's still active.""" -- cgit v1.2.3 From 2cb8db6172a0c273f2b5768483ecfc42edc8ef9c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 12:18:00 -0800 Subject: HelpChannels: add a function to init the categories As the categories are essential for the functionality of the cog, if this function fails to get a category, it will remove/unload the cog. --- bot/cogs/help_channels.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 5f5129149..5ca16fd41 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -49,6 +49,10 @@ class HelpChannels(Scheduler, commands.Cog): self.bot = bot + self.available_category: discord.CategoryChannel = None + self.in_use_category: discord.CategoryChannel = None + self.dormant_category: discord.CategoryChannel = None + async def create_channel_queue(self) -> asyncio.Queue: """Return a queue of dormant channels to use for getting the next available channel.""" @@ -71,6 +75,18 @@ class HelpChannels(Scheduler, commands.Cog): async def init_available(self) -> None: """Initialise the Available category with channels.""" + async def init_categories(self) -> None: + """Get the help category objects. Remove the cog if retrieval fails.""" + try: + self.available_category = await self.try_get_channel( + constants.Categories.help_available + ) + self.in_use_category = await self.try_get_channel(constants.Categories.help_in_use) + self.dormant_category = await self.try_get_channel(constants.Categories.help_dormant) + except discord.HTTPException: + log.exception(f"Failed to get a category; cog will be removed") + self.bot.remove_cog(self.qualified_name) + async def move_idle_channels(self) -> None: """Make all idle in-use channels dormant.""" -- cgit v1.2.3 From 67fd115fd95a003b0b248385a4175380a8959b1d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 12:24:12 -0800 Subject: HelpChannels: add a function to initialise the cog It's created as a task in __init__ because coroutines cannot be awaited in there. --- bot/cogs/help_channels.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 5ca16fd41..1e99f16b5 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -53,6 +53,11 @@ class HelpChannels(Scheduler, commands.Cog): self.in_use_category: discord.CategoryChannel = None self.dormant_category: discord.CategoryChannel = None + self.channel_queue: asyncio.Queue = None + self.name_queue: deque = None + + asyncio.create_task(self.init_cog()) + async def create_channel_queue(self) -> asyncio.Queue: """Return a queue of dormant channels to use for getting the next available channel.""" @@ -87,6 +92,18 @@ class HelpChannels(Scheduler, commands.Cog): log.exception(f"Failed to get a category; cog will be removed") self.bot.remove_cog(self.qualified_name) + async def init_cog(self) -> None: + """Initialise the help channel system.""" + await self.bot.wait_until_guild_available() + + await self.init_categories() + + self.channel_queue = await self.create_channel_queue() + self.name_queue = await self.name_queue() + + await self.init_available() + await self.move_idle_channels() + async def move_idle_channels(self) -> None: """Make all idle in-use channels dormant.""" -- cgit v1.2.3 From 4d7d29aef45d78a648deba4554d70eeff9691a4c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 12:25:29 -0800 Subject: HelpChannels: cancel the init task when unloading the cog This will prevent initialisation from proceeding when the category channels fail to be retrieved. --- bot/cogs/help_channels.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 1e99f16b5..3865183b0 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -56,7 +56,11 @@ class HelpChannels(Scheduler, commands.Cog): self.channel_queue: asyncio.Queue = None self.name_queue: deque = None - asyncio.create_task(self.init_cog()) + self.init_task = asyncio.create_task(self.init_cog()) + + async def cog_unload(self) -> None: + """Cancel the init task if the cog unloads.""" + self.init_task.cancel() async def create_channel_queue(self) -> asyncio.Queue: """Return a queue of dormant channels to use for getting the next available channel.""" -- cgit v1.2.3 From c5892c76f257cf14ddfad5cb4ccf47ff86623a47 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 12:27:02 -0800 Subject: HelpChannels: set a ready event when cog initialisation completes --- bot/cogs/help_channels.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 3865183b0..6dd689727 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -56,6 +56,7 @@ class HelpChannels(Scheduler, commands.Cog): self.channel_queue: asyncio.Queue = None self.name_queue: deque = None + self.ready = asyncio.Event() self.init_task = asyncio.create_task(self.init_cog()) async def cog_unload(self) -> None: @@ -108,6 +109,8 @@ class HelpChannels(Scheduler, commands.Cog): await self.init_available() await self.move_idle_channels() + self.ready.set() + async def move_idle_channels(self) -> None: """Make all idle in-use channels dormant.""" -- cgit v1.2.3 From 603b1c6e1ac02759e54a50ab892d3c529d10fa2e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 12:41:36 -0800 Subject: HelpChannels: add a function to return used channel names --- bot/cogs/help_channels.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 6dd689727..388bb1390 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,6 +1,8 @@ import asyncio +import itertools import json import logging +import typing as t from collections import deque from pathlib import Path @@ -79,6 +81,17 @@ class HelpChannels(Scheduler, commands.Cog): async def get_available_candidate(self) -> discord.TextChannel: """Return a dormant channel to turn into an available channel.""" + def get_used_names(self) -> t.Set[str]: + """Return channels names which are already being used.""" + start_index = len("help-") + channels = itertools.chain( + self.available_category.channels, + self.in_use_category.channels, + self.dormant_category.channels, + ) + + return {c.name[start_index:] for c in channels} + async def get_idle_time(self, channel: discord.TextChannel) -> int: """Return the time elapsed since the last message sent in the `channel`.""" -- cgit v1.2.3 From 0e1f40b1b0155cc16529bb13585021789695e2db Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 12:43:57 -0800 Subject: HelpChannels: implement create_name_queue It returns a queue of element names to use for creating new channels, taking into account which names are already being used. --- bot/cogs/help_channels.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 388bb1390..edc15607a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -71,8 +71,12 @@ class HelpChannels(Scheduler, commands.Cog): async def create_dormant(self) -> discord.TextChannel: """Create and return a new channel in the Dormant category.""" - async def create_name_queue(self) -> deque: + def create_name_queue(self) -> deque: """Return a queue of element names to use for creating new channels.""" + used_names = self.get_used_names() + available_names = (name for name in ELEMENTS if name not in used_names) + + return deque(available_names) @commands.command(name="dormant") async def dormant_command(self) -> None: -- cgit v1.2.3 From 8d968529cd27b61dbc12f41f96d850f0ddeab66b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 13:02:05 -0800 Subject: HelpChannels: retrieve category channels more efficiently The channels property of categories sorts the channels before returning them. * Add a generator function to get category channels --- bot/cogs/help_channels.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index edc15607a..1ba435308 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,5 +1,4 @@ import asyncio -import itertools import json import logging import typing as t @@ -85,16 +84,25 @@ class HelpChannels(Scheduler, commands.Cog): async def get_available_candidate(self) -> discord.TextChannel: """Return a dormant channel to turn into an available channel.""" + @staticmethod + def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: + """Yield the channels of the `category` in an unsorted manner.""" + # This is faster than using category.channels because the latter sorts them. + for channel in category.guild.channels: + if channel.category_id == category.id: + yield channel + def get_used_names(self) -> t.Set[str]: """Return channels names which are already being used.""" start_index = len("help-") - channels = itertools.chain( - self.available_category.channels, - self.in_use_category.channels, - self.dormant_category.channels, - ) - return {c.name[start_index:] for c in channels} + names = set() + for cat in (self.available_category, self.in_use_category, self.dormant_category): + for channel in self.get_category_channels(cat): + name = channel.name[start_index:] + names.add(name) + + return names async def get_idle_time(self, channel: discord.TextChannel) -> int: """Return the time elapsed since the last message sent in the `channel`.""" -- cgit v1.2.3 From b64524d56a407a21f85b08b7ae7147fa13283543 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 13:08:19 -0800 Subject: HelpChannels: only yield text channels from a category --- bot/cogs/help_channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 1ba435308..f7af5d3be 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -86,10 +86,10 @@ class HelpChannels(Scheduler, commands.Cog): @staticmethod def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: - """Yield the channels of the `category` in an unsorted manner.""" + """Yield the text channels of the `category` in an unsorted manner.""" # This is faster than using category.channels because the latter sorts them. for channel in category.guild.channels: - if channel.category_id == category.id: + if channel.category_id == category.id and isinstance(channel, discord.TextChannel): yield channel def get_used_names(self) -> t.Set[str]: -- cgit v1.2.3 From db482932088a3d648079cf1b04e0dc89f2602105 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 13:11:56 -0800 Subject: HelpChannels: implement create_channel_queue It returns a queue of dormant channels in random order. The queue will be used to get the next available channel. Using a random order is simpler than trying to sort by the timestamp of the most recent message in each channel and this decision will only "negatively" impact the system when the bot restarts or the extension is reloaded. Ultimately, it just means in such events some dormant channels may chosen to become active again sooner than expected. --- bot/cogs/help_channels.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index f7af5d3be..3d7ece909 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import random import typing as t from collections import deque from pathlib import Path @@ -64,8 +65,20 @@ class HelpChannels(Scheduler, commands.Cog): """Cancel the init task if the cog unloads.""" self.init_task.cancel() - async def create_channel_queue(self) -> asyncio.Queue: - """Return a queue of dormant channels to use for getting the next available channel.""" + def create_channel_queue(self) -> asyncio.Queue: + """ + Return a queue of dormant channels to use for getting the next available channel. + + The channels are added to the queue in a random order. + """ + channels = list(self.get_category_channels(self.dormant_category)) + random.shuffle(channels) + + queue = asyncio.Queue() + for channel in channels: + queue.put_nowait(channel) + + return queue async def create_dormant(self) -> discord.TextChannel: """Create and return a new channel in the Dormant category.""" -- cgit v1.2.3 From ca995f96292d8ff334696e89353e977999f4b6b0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 13:23:51 -0800 Subject: Constants: add a help channel name prefix constant --- bot/cogs/help_channels.py | 2 +- bot/constants.py | 1 + config-default.yml | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 3d7ece909..026cb1f78 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -107,7 +107,7 @@ class HelpChannels(Scheduler, commands.Cog): def get_used_names(self) -> t.Set[str]: """Return channels names which are already being used.""" - start_index = len("help-") + start_index = len(constants.HelpChannels.name_prefix) names = set() for cat in (self.available_category, self.in_use_category, self.dormant_category): diff --git a/bot/constants.py b/bot/constants.py index 58a236546..5b50050d6 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -540,6 +540,7 @@ class HelpChannels(metaclass=YAMLGetter): cmd_whitelist: List[int] idle_minutes: int max_available: int + name_prefix: str class Mention(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index 27143ff30..c095aa30b 100644 --- a/config-default.yml +++ b/config-default.yml @@ -525,6 +525,9 @@ help_channels: # Maximum number of channels to put in the available category max_available: 2 + # Prefix for help channel names + name_prefix: 'help-' + redirect_output: delete_invocation: true delete_delay: 15 -- cgit v1.2.3 From 72e564a6704850a54831aab5c4d9d777a21a4f27 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 14:30:55 -0800 Subject: Constants: implement init_available Initialises the Available category with channels if any are missing. --- bot/cogs/help_channels.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 026cb1f78..06520fc08 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -122,6 +122,11 @@ class HelpChannels(Scheduler, commands.Cog): async def init_available(self) -> None: """Initialise the Available category with channels.""" + channels = list(self.get_category_channels(self.available_category)) + missing = constants.HelpChannels.max_available - len(channels) + + for _ in range(missing): + await self.move_to_available() async def init_categories(self) -> None: """Get the help category objects. Remove the cog if retrieval fails.""" -- cgit v1.2.3 From 7c4b776847e7c857c09d43a2434d1187bbb354b5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 14:45:06 -0800 Subject: Constants: add a named tuple for scheduled task data --- bot/cogs/help_channels.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 06520fc08..c440d166c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -43,6 +43,13 @@ with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file ELEMENTS = json.load(elements_file) +class ChannelTimeout(t.NamedTuple): + """Data for a task scheduled to make a channel dormant.""" + + channel: discord.TextChannel + timeout: int + + class HelpChannels(Scheduler, commands.Cog): """Manage the help channel system of the guild.""" @@ -175,8 +182,8 @@ class HelpChannels(Scheduler, commands.Cog): return channel - async def _scheduled_task(self, channel: discord.TextChannel, timeout: int) -> None: - """Make the `channel` dormant after `timeout` seconds or reschedule if it's still active.""" + async def _scheduled_task(self, data: ChannelTimeout) -> None: + """Make a channel dormant after specified timeout or reschedule if it's still active.""" def setup(bot: Bot) -> None: -- cgit v1.2.3 From cef96afb64d0456e1b60b9be68fb0352bd0191a1 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 14:55:13 -0800 Subject: HelpChannels: implement move_idle_channels Make all in-use channels dormant if idle or schedule the move if still active. This is intended to clean up the in-use channels when the bot restarts and has lost the tasks it had scheduled in another life. --- bot/cogs/help_channels.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index c440d166c..c8c437145 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -162,7 +162,16 @@ class HelpChannels(Scheduler, commands.Cog): self.ready.set() async def move_idle_channels(self) -> None: - """Make all idle in-use channels dormant.""" + """Make all in-use channels dormant if idle or schedule the move if still active.""" + idle_seconds = constants.HelpChannels.idle_minutes * 60 + + for channel in self.get_category_channels(self.in_use_category): + time_elapsed = await self.get_idle_time(channel) + if time_elapsed > idle_seconds: + await self.move_to_dormant(channel) + else: + data = ChannelTimeout(channel, idle_seconds - time_elapsed) + self.schedule_task(self.bot.loop, channel.id, data) async def move_to_available(self) -> None: """Make a channel available.""" -- cgit v1.2.3 From 6f9167b3cc016b55265e9692c930924a751a3e10 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 14:56:53 -0800 Subject: HelpChannels: fix creation of queues in init_cog * Remove await from create_channel_queue * Call the correct function to create the name queue --- bot/cogs/help_channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index c8c437145..3757f0581 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -153,8 +153,8 @@ class HelpChannels(Scheduler, commands.Cog): await self.init_categories() - self.channel_queue = await self.create_channel_queue() - self.name_queue = await self.name_queue() + self.channel_queue = self.create_channel_queue() + self.name_queue = self.create_name_queue() await self.init_available() await self.move_idle_channels() -- cgit v1.2.3 From 6c57fc1d6581da53394871c9967f7de2fc0ec25f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 15:08:05 -0800 Subject: HelpChannels: make move_idle_channels only handle a single channel This function will get re-used in _scheduled_task, but it will only need to move a single channel. Therefore, to promote code re-use, this change was made. The init_cog will instead do a loop to call this on all channels in the in-use category. --- bot/cogs/help_channels.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 3757f0581..7fe81d407 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -157,21 +157,22 @@ class HelpChannels(Scheduler, commands.Cog): self.name_queue = self.create_name_queue() await self.init_available() - await self.move_idle_channels() + + for channel in self.get_category_channels(self.in_use_category): + await self.move_idle_channel(channel) self.ready.set() - async def move_idle_channels(self) -> None: - """Make all in-use channels dormant if idle or schedule the move if still active.""" + async def move_idle_channel(self, channel: discord.TextChannel) -> None: + """Make the `channel` dormant if idle or schedule the move if still active.""" idle_seconds = constants.HelpChannels.idle_minutes * 60 + time_elapsed = await self.get_idle_time(channel) - for channel in self.get_category_channels(self.in_use_category): - time_elapsed = await self.get_idle_time(channel) - if time_elapsed > idle_seconds: - await self.move_to_dormant(channel) - else: - data = ChannelTimeout(channel, idle_seconds - time_elapsed) - self.schedule_task(self.bot.loop, channel.id, data) + if time_elapsed > idle_seconds: + await self.move_to_dormant(channel) + else: + data = ChannelTimeout(channel, idle_seconds - time_elapsed) + self.schedule_task(self.bot.loop, channel.id, data) async def move_to_available(self) -> None: """Make a channel available.""" -- cgit v1.2.3 From b1aef7df897bdc7f1775e623a57052768557649a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 15:12:25 -0800 Subject: HelpChannels: implement get_idle_time A design change was made to account for a channel being empty i.e. no messages ever sent. In such case, the function will return None. * Move a channel to the Dormant category if the channel has no messages --- bot/cogs/help_channels.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 7fe81d407..a848a3029 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -4,6 +4,7 @@ import logging import random import typing as t from collections import deque +from datetime import datetime from pathlib import Path import discord @@ -124,8 +125,19 @@ class HelpChannels(Scheduler, commands.Cog): return names - async def get_idle_time(self, channel: discord.TextChannel) -> int: - """Return the time elapsed since the last message sent in the `channel`.""" + @staticmethod + async def get_idle_time(channel: discord.TextChannel) -> t.Optional[int]: + """ + Return the time elapsed, in seconds, since the last message sent in the `channel`. + + Return None if the channel has no messages. + """ + try: + msg = await channel.history(limit=1).next() # noqa: B305 + except discord.NoMoreItems: + return None + + return (datetime.utcnow() - msg.created_at).seconds async def init_available(self) -> None: """Initialise the Available category with channels.""" @@ -168,7 +180,7 @@ class HelpChannels(Scheduler, commands.Cog): idle_seconds = constants.HelpChannels.idle_minutes * 60 time_elapsed = await self.get_idle_time(channel) - if time_elapsed > idle_seconds: + if time_elapsed is None or time_elapsed > idle_seconds: await self.move_to_dormant(channel) else: data = ChannelTimeout(channel, idle_seconds - time_elapsed) -- cgit v1.2.3 From 9f871a9d384ba2754bae047d62fb8a1bfd7e2141 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 15:19:39 -0800 Subject: HelpChannels: implement create_dormant Create and return a new channel in the Dormant category or return None if no names remain. The overwrites get synced with the category if none are explicitly specified for the channel. --- bot/cogs/help_channels.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index a848a3029..4a34bd37d 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -88,8 +88,22 @@ class HelpChannels(Scheduler, commands.Cog): return queue - async def create_dormant(self) -> discord.TextChannel: - """Create and return a new channel in the Dormant category.""" + async def create_dormant(self) -> t.Optional[discord.TextChannel]: + """ + Create and return a new channel in the Dormant category. + + The new channel will sync its permission overwrites with the category. + + Return None if no more channel names are available. + """ + name = constants.HelpChannels.name_prefix + + try: + name += self.name_queue.popleft() + except IndexError: + return None + + return await self.dormant_category.create_text_channel(name) def create_name_queue(self) -> deque: """Return a queue of element names to use for creating new channels.""" -- cgit v1.2.3 From 021fcbb20816f2031c9b190fb0c91d3e9b709b59 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 15:29:21 -0800 Subject: HelpChannels: implement get_available_candidate Return a dormant channel to turn into an available channel, waiting indefinitely until one becomes available in the queue. --- bot/cogs/help_channels.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 4a34bd37d..99815d4e5 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -117,7 +117,21 @@ class HelpChannels(Scheduler, commands.Cog): """Make the current in-use help channel dormant.""" async def get_available_candidate(self) -> discord.TextChannel: - """Return a dormant channel to turn into an available channel.""" + """ + Return a dormant channel to turn into an available channel. + + If no channel is available, wait indefinitely until one becomes available. + """ + try: + channel = self.channel_queue.get_nowait() + except asyncio.QueueEmpty: + channel = await self.create_dormant() + + if not channel: + # Wait for a channel to become available. + channel = await self.channel_queue.get() + + return channel @staticmethod def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: -- cgit v1.2.3 From 24bdb303547f7d03eaa7ed8cd7720e5cc0c91e8b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 15:57:28 -0800 Subject: HelpChannels: implement move_to_available Moves a channel to the Available category. Permissions will be synced with the new category. * Add stubs for channel topic constants --- bot/cogs/help_channels.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 99815d4e5..5e27757f7 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -16,6 +16,10 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) +# TODO: write the channel topics +AVAILABLE_TOPIC = "" +IN_USE_TOPIC = "" +DORMANT_TOPIC = "" ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" AVAILABLE_MSG = f""" @@ -216,6 +220,16 @@ class HelpChannels(Scheduler, commands.Cog): async def move_to_available(self) -> None: """Make a channel available.""" + channel = await self.get_available_candidate() + embed = discord.Embed(description=AVAILABLE_MSG) + + # TODO: edit or delete the dormant message + await channel.send(embed=embed) + await channel.edit( + category=self.available_category, + sync_permissions=True, + topic=AVAILABLE_TOPIC, + ) async def move_to_dormant(self, channel: discord.TextChannel) -> None: """Make the `channel` dormant.""" -- cgit v1.2.3 From 0c38d732da774f75c6c786b2fae5daaea6547b82 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 15:58:50 -0800 Subject: HelpChannels: implement move_to_dormant Moves a channel to the Dormant category. Permissions will be synced with the new category. --- bot/cogs/help_channels.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 5e27757f7..9ef7fc72c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -233,6 +233,14 @@ class HelpChannels(Scheduler, commands.Cog): async def move_to_dormant(self, channel: discord.TextChannel) -> None: """Make the `channel` dormant.""" + await channel.edit( + category=self.dormant_category, + sync_permissions=True, + topic=DORMANT_TOPIC, + ) + + embed = discord.Embed(description=DORMANT_MSG) + await channel.send(embed=embed) @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: -- cgit v1.2.3 From 86bb25814d664442e4f4643d934c182b6f77107e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 16:35:57 -0800 Subject: HelpChannels: implement the !dormant command Basically a wrapper around move_to_dormant which ensures the current channel is in use. If it's not in-use, from the invoker's perspective, the command silently fails (it does at least log). InChannelCheckFailure was considered but it seemed like it'd be too spammy, especially if there'd be a long list of allowed channels. --- bot/cogs/help_channels.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 9ef7fc72c..b4121c7fd 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -12,6 +12,7 @@ from discord.ext import commands from bot import constants from bot.bot import Bot +from bot.decorators import with_role from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) @@ -117,8 +118,14 @@ class HelpChannels(Scheduler, commands.Cog): return deque(available_names) @commands.command(name="dormant") - async def dormant_command(self) -> None: + @with_role(*constants.HelpChannels.cmd_whitelist) + async def dormant_command(self, ctx: commands.Context) -> None: """Make the current in-use help channel dormant.""" + in_use = self.get_category_channels(self.in_use_category) + if ctx.channel in in_use: + await self.move_to_dormant(ctx.channel) + else: + log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") async def get_available_candidate(self) -> discord.TextChannel: """ -- cgit v1.2.3 From 7de241bd6ca7d156e3014611596d6bcb969f9c96 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 16:48:57 -0800 Subject: HelpChannels: add a function to make channels in-use It handles moving the channel to the category and scheduling it to be made dormant. --- bot/cogs/help_channels.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index b4121c7fd..806020873 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -249,6 +249,19 @@ class HelpChannels(Scheduler, commands.Cog): embed = discord.Embed(description=DORMANT_MSG) await channel.send(embed=embed) + async def move_to_in_use(self, channel: discord.TextChannel) -> None: + """Make a channel in-use and schedule it to be made dormant.""" + # Move the channel to the In Use category. + await channel.edit( + category=self.in_use_category, + sync_permissions=True, + topic=IN_USE_TOPIC, + ) + + # Schedule the channel to be moved to the Dormant category. + data = ChannelTimeout(channel, constants.HelpChannels.idle_minutes * 60) + self.schedule_task(self.bot.loop, channel.id, data) + @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Move an available channel to the In Use category and replace it with a dormant one.""" -- cgit v1.2.3 From 0595b550111cf684721f85a0e340880c9f15288a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 16:49:45 -0800 Subject: HelpChannels: implement the on_message listener It handles making channels in-use and replacing them with new available channels. --- bot/cogs/help_channels.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 806020873..6b77f9955 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -265,6 +265,15 @@ class HelpChannels(Scheduler, commands.Cog): @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Move an available channel to the In Use category and replace it with a dormant one.""" + available_channels = self.get_category_channels(self.available_category) + if message.channel not in available_channels: + return # Ignore messages outside the Available category. + + await self.move_to_in_use(message.channel) + + # Move a dormant channel to the Available category to fill in the gap. + # This is done last because it may wait indefinitely for a channel to be put in the queue. + await self.move_to_available() async def try_get_channel(self, channel_id: int) -> discord.abc.GuildChannel: """Attempt to get or fetch a channel and return it.""" -- cgit v1.2.3 From f3b54c2f35d2ab11e6ac88c94b1729fb2b86b781 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 16:53:22 -0800 Subject: HelpChannels: cancel scheduled tasks when the cog unloads * Make cog_unload a regular method instead of a coroutine --- bot/cogs/help_channels.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 6b77f9955..f493e5918 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -74,10 +74,13 @@ class HelpChannels(Scheduler, commands.Cog): self.ready = asyncio.Event() self.init_task = asyncio.create_task(self.init_cog()) - async def cog_unload(self) -> None: - """Cancel the init task if the cog unloads.""" + def cog_unload(self) -> None: + """Cancel the init task and scheduled tasks when the cog unloads.""" self.init_task.cancel() + for task in self.scheduled_tasks.values(): + task.cancel() + def create_channel_queue(self) -> asyncio.Queue: """ Return a queue of dormant channels to use for getting the next available channel. -- cgit v1.2.3 From c1e485b11dee6f275a4c499e8f9be6cdde9e5e7a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 17:01:34 -0800 Subject: HelpChannels: cancel an existing task before scheduling a new one --- bot/cogs/help_channels.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index f493e5918..c547d9524 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -218,13 +218,21 @@ class HelpChannels(Scheduler, commands.Cog): self.ready.set() async def move_idle_channel(self, channel: discord.TextChannel) -> None: - """Make the `channel` dormant if idle or schedule the move if still active.""" + """ + Make the `channel` dormant if idle or schedule the move if still active. + + If a task to make the channel dormant already exists, it will first be cancelled. + """ idle_seconds = constants.HelpChannels.idle_minutes * 60 time_elapsed = await self.get_idle_time(channel) if time_elapsed is None or time_elapsed > idle_seconds: await self.move_to_dormant(channel) else: + # Cancel the existing task, if any. + if channel.id in self.scheduled_tasks: + self.cancel_task(channel.id) + data = ChannelTimeout(channel, idle_seconds - time_elapsed) self.schedule_task(self.bot.loop, channel.id, data) -- cgit v1.2.3 From 4c43e6e41be365c9134bacfb690fca55cb68f81f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 17:05:44 -0800 Subject: HelpChannels: implement _scheduled_task Make a channel dormant after specified timeout or reschedule if it's still active. --- bot/cogs/help_channels.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index c547d9524..12bed2e61 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -296,6 +296,11 @@ class HelpChannels(Scheduler, commands.Cog): async def _scheduled_task(self, data: ChannelTimeout) -> None: """Make a channel dormant after specified timeout or reschedule if it's still active.""" + await asyncio.sleep(data.timeout) + + # Use asyncio.shield to prevent move_idle_channel from cancelling itself. + # The parent task (_scheduled_task) will still get cancelled. + await asyncio.shield(self.move_idle_channel(data.channel)) def setup(bot: Bot) -> None: -- cgit v1.2.3 From d66c284034dec8352dc8a20ec1cb978c47f93d3d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 17:14:08 -0800 Subject: HelpChannels: wait for cog to be initialised before processing messages --- bot/cogs/help_channels.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 12bed2e61..43ce59cf1 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -276,6 +276,8 @@ class HelpChannels(Scheduler, commands.Cog): @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Move an available channel to the In Use category and replace it with a dormant one.""" + await self.ready.wait() + available_channels = self.get_category_channels(self.available_category) if message.channel not in available_channels: return # Ignore messages outside the Available category. -- cgit v1.2.3 From e1fb742253546b57611eb562fa0b1c839941a864 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 17:37:36 -0800 Subject: HelpChannels: use a lock to prevent a channel from being processed twice --- bot/cogs/help_channels.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 43ce59cf1..fd5632d09 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -72,6 +72,7 @@ class HelpChannels(Scheduler, commands.Cog): self.name_queue: deque = None self.ready = asyncio.Event() + self.on_message_lock = asyncio.Lock() self.init_task = asyncio.create_task(self.init_cog()) def cog_unload(self) -> None: @@ -278,14 +279,17 @@ class HelpChannels(Scheduler, commands.Cog): """Move an available channel to the In Use category and replace it with a dormant one.""" await self.ready.wait() - available_channels = self.get_category_channels(self.available_category) - if message.channel not in available_channels: - return # Ignore messages outside the Available category. + # Use a lock to prevent a channel from being processed twice. + with self.on_message_lock.acquire(): + available_channels = self.get_category_channels(self.available_category) + if message.channel not in available_channels: + return # Ignore messages outside the Available category. - await self.move_to_in_use(message.channel) + await self.move_to_in_use(message.channel) # Move a dormant channel to the Available category to fill in the gap. - # This is done last because it may wait indefinitely for a channel to be put in the queue. + # This is done last and outside the lock because it may wait indefinitely for a channel to + # be put in the queue. await self.move_to_available() async def try_get_channel(self, channel_id: int) -> discord.abc.GuildChannel: -- cgit v1.2.3 From 96ed02a565feabcc9415ae8909792323b08f9b08 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 18:26:09 -0800 Subject: HelpChannels: add logging --- bot/cogs/help_channels.py | 89 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index fd5632d09..82dce4ee7 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -77,8 +77,10 @@ class HelpChannels(Scheduler, commands.Cog): def cog_unload(self) -> None: """Cancel the init task and scheduled tasks when the cog unloads.""" + log.trace("Cog unload: cancelling the cog_init task") self.init_task.cancel() + log.trace("Cog unload: cancelling the scheduled tasks") for task in self.scheduled_tasks.values(): task.cancel() @@ -88,9 +90,12 @@ class HelpChannels(Scheduler, commands.Cog): The channels are added to the queue in a random order. """ + log.trace("Creating the channel queue.") + channels = list(self.get_category_channels(self.dormant_category)) random.shuffle(channels) + log.trace("Populating the channel queue with channels.") queue = asyncio.Queue() for channel in channels: queue.put_nowait(channel) @@ -105,26 +110,36 @@ class HelpChannels(Scheduler, commands.Cog): Return None if no more channel names are available. """ + log.trace("Getting a name for a new dormant channel.") name = constants.HelpChannels.name_prefix try: name += self.name_queue.popleft() except IndexError: + log.debug("No more names available for new dormant channels.") return None + log.debug(f"Creating a new dormant channel named {name}.") return await self.dormant_category.create_text_channel(name) def create_name_queue(self) -> deque: """Return a queue of element names to use for creating new channels.""" + log.trace("Creating the chemical element name queue.") + used_names = self.get_used_names() + + log.trace("Determining the available names.") available_names = (name for name in ELEMENTS if name not in used_names) + log.trace("Populating the name queue with names.") return deque(available_names) @commands.command(name="dormant") @with_role(*constants.HelpChannels.cmd_whitelist) async def dormant_command(self, ctx: commands.Context) -> None: """Make the current in-use help channel dormant.""" + log.trace("dormant command invoked; checking if the channel is in-use.") + in_use = self.get_category_channels(self.in_use_category) if ctx.channel in in_use: await self.move_to_dormant(ctx.channel) @@ -137,13 +152,16 @@ class HelpChannels(Scheduler, commands.Cog): If no channel is available, wait indefinitely until one becomes available. """ + log.trace("Getting an available channel candidate.") + try: channel = self.channel_queue.get_nowait() except asyncio.QueueEmpty: + log.info("No candidate channels in the queue; creating a new channel.") channel = await self.create_dormant() if not channel: - # Wait for a channel to become available. + log.info("Couldn't create a candidate channel; waiting to get one from the queue.") channel = await self.channel_queue.get() return channel @@ -151,6 +169,8 @@ class HelpChannels(Scheduler, commands.Cog): @staticmethod def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: """Yield the text channels of the `category` in an unsorted manner.""" + log.trace(f"Getting text channels in the category '{category.name}' ({category.id}).") + # This is faster than using category.channels because the latter sorts them. for channel in category.guild.channels: if channel.category_id == category.id and isinstance(channel, discord.TextChannel): @@ -158,6 +178,8 @@ class HelpChannels(Scheduler, commands.Cog): def get_used_names(self) -> t.Set[str]: """Return channels names which are already being used.""" + log.trace("Getting channel names which are already being used.") + start_index = len(constants.HelpChannels.name_prefix) names = set() @@ -166,6 +188,7 @@ class HelpChannels(Scheduler, commands.Cog): name = channel.name[start_index:] names.add(name) + log.trace(f"Got {len(names)} used names: {names}") return names @staticmethod @@ -175,23 +198,35 @@ class HelpChannels(Scheduler, commands.Cog): Return None if the channel has no messages. """ + log.trace(f"Getting the idle time for #{channel.name} ({channel.id}).") + try: msg = await channel.history(limit=1).next() # noqa: B305 except discord.NoMoreItems: + log.debug(f"No idle time available; #{channel.name} ({channel.id}) has no messages.") return None - return (datetime.utcnow() - msg.created_at).seconds + idle_time = (datetime.utcnow() - msg.created_at).seconds + + log.trace(f"#{channel.name} ({channel.id}) has been idle for {idle_time} seconds.") + return idle_time async def init_available(self) -> None: """Initialise the Available category with channels.""" + log.trace("Initialising the Available category with channels.") + channels = list(self.get_category_channels(self.available_category)) missing = constants.HelpChannels.max_available - len(channels) + log.trace(f"Moving {missing} missing channels to the Available category.") + for _ in range(missing): await self.move_to_available() async def init_categories(self) -> None: """Get the help category objects. Remove the cog if retrieval fails.""" + log.trace("Getting the CategoryChannel objects for the help categories.") + try: self.available_category = await self.try_get_channel( constants.Categories.help_available @@ -204,8 +239,10 @@ class HelpChannels(Scheduler, commands.Cog): async def init_cog(self) -> None: """Initialise the help channel system.""" + log.trace("Waiting for the guild to be available before initialisation.") await self.bot.wait_until_guild_available() + log.trace("Initialising the cog.") await self.init_categories() self.channel_queue = self.create_channel_queue() @@ -213,9 +250,11 @@ class HelpChannels(Scheduler, commands.Cog): await self.init_available() + log.trace("Moving or rescheduling in-use channels.") for channel in self.get_category_channels(self.in_use_category): await self.move_idle_channel(channel) + log.info("Cog is ready!") self.ready.set() async def move_idle_channel(self, channel: discord.TextChannel) -> None: @@ -224,10 +263,17 @@ class HelpChannels(Scheduler, commands.Cog): If a task to make the channel dormant already exists, it will first be cancelled. """ + log.trace(f"Handling in-use channel #{channel.name} ({channel.id}).") + idle_seconds = constants.HelpChannels.idle_minutes * 60 time_elapsed = await self.get_idle_time(channel) if time_elapsed is None or time_elapsed > idle_seconds: + log.info( + f"#{channel.name} ({channel.id}) is idle longer than {idle_seconds} seconds " + f"and will be made dormant." + ) + await self.move_to_dormant(channel) else: # Cancel the existing task, if any. @@ -235,15 +281,28 @@ class HelpChannels(Scheduler, commands.Cog): self.cancel_task(channel.id) data = ChannelTimeout(channel, idle_seconds - time_elapsed) + + log.info( + f"#{channel.name} ({channel.id}) is still active; " + f"scheduling it to be moved after {data.timeout} seconds." + ) + self.schedule_task(self.bot.loop, channel.id, data) async def move_to_available(self) -> None: """Make a channel available.""" + log.trace("Making a channel available.") + channel = await self.get_available_candidate() embed = discord.Embed(description=AVAILABLE_MSG) + log.info(f"Making #{channel.name} ({channel.id}) available.") + # TODO: edit or delete the dormant message + log.trace(f"Sending available message for #{channel.name} ({channel.id}).") await channel.send(embed=embed) + + log.trace(f"Moving #{channel.name} ({channel.id}) to the Available category.") await channel.edit( category=self.available_category, sync_permissions=True, @@ -252,40 +311,53 @@ class HelpChannels(Scheduler, commands.Cog): async def move_to_dormant(self, channel: discord.TextChannel) -> None: """Make the `channel` dormant.""" + log.info(f"Making #{channel.name} ({channel.id}) dormant.") + + log.trace(f"Moving #{channel.name} ({channel.id}) to the Dormant category.") await channel.edit( category=self.dormant_category, sync_permissions=True, topic=DORMANT_TOPIC, ) + log.trace(f"Sending dormant message for #{channel.name} ({channel.id}).") embed = discord.Embed(description=DORMANT_MSG) await channel.send(embed=embed) async def move_to_in_use(self, channel: discord.TextChannel) -> None: """Make a channel in-use and schedule it to be made dormant.""" - # Move the channel to the In Use category. + log.info(f"Making #{channel.name} ({channel.id}) in-use.") + + log.trace(f"Moving #{channel.name} ({channel.id}) to the In Use category.") await channel.edit( category=self.in_use_category, sync_permissions=True, topic=IN_USE_TOPIC, ) - # Schedule the channel to be moved to the Dormant category. - data = ChannelTimeout(channel, constants.HelpChannels.idle_minutes * 60) + timeout = constants.HelpChannels.idle_minutes * 60 + + log.trace(f"Scheduling #{channel.name} ({channel.id}) to become dormant in {timeout} sec.") + data = ChannelTimeout(channel, timeout) self.schedule_task(self.bot.loop, channel.id, data) @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Move an available channel to the In Use category and replace it with a dormant one.""" + log.trace("Waiting for the cog to be ready before processing messages.") await self.ready.wait() - # Use a lock to prevent a channel from being processed twice. + log.trace("Acquiring lock to prevent a channel from being processed twice...") with self.on_message_lock.acquire(): + log.trace("on_message lock acquired.") + log.trace("Checking if the message was sent in an available channel.") + available_channels = self.get_category_channels(self.available_category) if message.channel not in available_channels: return # Ignore messages outside the Available category. await self.move_to_in_use(message.channel) + log.trace("Releasing on_message lock.") # Move a dormant channel to the Available category to fill in the gap. # This is done last and outside the lock because it may wait indefinitely for a channel to @@ -294,14 +366,19 @@ class HelpChannels(Scheduler, commands.Cog): async def try_get_channel(self, channel_id: int) -> discord.abc.GuildChannel: """Attempt to get or fetch a channel and return it.""" + log.trace(f"Getting the channel {channel_id}.") + channel = self.bot.get_channel(channel_id) if not channel: + log.debug(f"Channel {channel_id} is not in cache; fetching from API.") channel = await self.bot.fetch_channel(channel_id) + log.trace(f"Channel #{channel.name} ({channel_id}) retrieved.") return channel async def _scheduled_task(self, data: ChannelTimeout) -> None: """Make a channel dormant after specified timeout or reschedule if it's still active.""" + log.trace(f"Waiting {data.timeout} before making #{data.channel.name} dormant.") await asyncio.sleep(data.timeout) # Use asyncio.shield to prevent move_idle_channel from cancelling itself. -- cgit v1.2.3 From 886692e3782a330ce0f6aab3b0a9612b65256736 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 18:27:33 -0800 Subject: Bot: load the help channels extension --- bot/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/__main__.py b/bot/__main__.py index 3df477a6d..7ca6eabce 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -50,6 +50,7 @@ bot.load_extension("bot.cogs.defcon") bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.duck_pond") bot.load_extension("bot.cogs.free") +bot.load_extension("bot.cogs.help_channels") bot.load_extension("bot.cogs.information") bot.load_extension("bot.cogs.jams") bot.load_extension("bot.cogs.moderation") -- cgit v1.2.3 From 8e4b14052a887489b93366f6066f0636657c2570 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 18:28:23 -0800 Subject: Remove the free extension Obsolete due to the new help channel system. --- bot/__main__.py | 1 - bot/cogs/free.py | 103 ------------------------------------------------------- 2 files changed, 104 deletions(-) delete mode 100644 bot/cogs/free.py diff --git a/bot/__main__.py b/bot/__main__.py index 7ca6eabce..30a7dee41 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -49,7 +49,6 @@ bot.load_extension("bot.cogs.alias") bot.load_extension("bot.cogs.defcon") bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.duck_pond") -bot.load_extension("bot.cogs.free") bot.load_extension("bot.cogs.help_channels") bot.load_extension("bot.cogs.information") bot.load_extension("bot.cogs.jams") diff --git a/bot/cogs/free.py b/bot/cogs/free.py deleted file mode 100644 index 99516fade..000000000 --- a/bot/cogs/free.py +++ /dev/null @@ -1,103 +0,0 @@ -import logging -from datetime import datetime -from operator import itemgetter - -from discord import Colour, Embed, Member, utils -from discord.ext.commands import Cog, Context, command - -from bot.bot import Bot -from bot.constants import Categories, Channels, Free, STAFF_ROLES -from bot.decorators import redirect_output - -log = logging.getLogger(__name__) - -TIMEOUT = Free.activity_timeout -RATE = Free.cooldown_rate -PER = Free.cooldown_per - - -class Free(Cog): - """Tries to figure out which help channels are free.""" - - PYTHON_HELP_ID = Categories.help_in_use - - @command(name="free", aliases=('f',)) - @redirect_output(destination_channel=Channels.bot_commands, bypass_roles=STAFF_ROLES) - async def free(self, ctx: Context, user: Member = None, seek: int = 2) -> None: - """ - Lists free help channels by likeliness of availability. - - seek is used only when this command is invoked in a help channel. - You cannot override seek without mentioning a user first. - - When seek is 2, we are avoiding considering the last active message - in a channel to be the one that invoked this command. - - When seek is 3 or more, a user has been mentioned on the assumption - that they asked if the channel is free or they asked their question - in an active channel, and we want the message before that happened. - """ - free_channels = [] - python_help = utils.get(ctx.guild.categories, id=self.PYTHON_HELP_ID) - - if user is not None and seek == 2: - seek = 3 - elif not 0 < seek < 10: - seek = 3 - - # Iterate through all the help channels - # to check latest activity - for channel in python_help.channels: - # Seek further back in the help channel - # the command was invoked in - if channel.id == ctx.channel.id: - messages = await channel.history(limit=seek).flatten() - msg = messages[seek - 1] - # Otherwise get last message - else: - msg = await channel.history(limit=1).next() # noqa: B305 - - inactive = (datetime.utcnow() - msg.created_at).seconds - if inactive > TIMEOUT: - free_channels.append((inactive, channel)) - - embed = Embed() - embed.colour = Colour.blurple() - embed.title = "**Looking for a free help channel?**" - - if user is not None: - embed.description = f"**Hey {user.mention}!**\n\n" - else: - embed.description = "" - - # Display all potentially inactive channels - # in descending order of inactivity - if free_channels: - # Sort channels in descending order by seconds - # Get position in list, inactivity, and channel object - # For each channel, add to embed.description - sorted_channels = sorted(free_channels, key=itemgetter(0), reverse=True) - - for (inactive, channel) in sorted_channels[:3]: - minutes, seconds = divmod(inactive, 60) - if minutes > 59: - hours, minutes = divmod(minutes, 60) - embed.description += f"{channel.mention} **{hours}h {minutes}m {seconds}s** inactive\n" - else: - embed.description += f"{channel.mention} **{minutes}m {seconds}s** inactive\n" - - embed.set_footer(text="Please confirm these channels are free before posting") - else: - embed.description = ( - "Doesn't look like any channels are available right now. " - "You're welcome to check for yourself to be sure. " - "If all channels are truly busy, please be patient " - "as one will likely be available soon." - ) - - await ctx.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the Free cog.""" - bot.add_cog(Free()) -- cgit v1.2.3 From ca4eb6bcd82a2bfaf620aaab0a7acee607051dc3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 18:33:55 -0800 Subject: HelpChannels: fix creation of the init_cog task The task has to be created on a specific loop because when the cog is instantiated, the event loop is not yet running. --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 82dce4ee7..391d400b1 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -73,7 +73,7 @@ class HelpChannels(Scheduler, commands.Cog): self.ready = asyncio.Event() self.on_message_lock = asyncio.Lock() - self.init_task = asyncio.create_task(self.init_cog()) + self.init_task = self.bot.loop.create_task(self.init_cog()) def cog_unload(self) -> None: """Cancel the init task and scheduled tasks when the cog unloads.""" -- cgit v1.2.3 From f5900f0f885f2e0c71a4accf8f680da09746070a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 18:41:47 -0800 Subject: Resources: make all element names lower cased --- bot/resources/elements.json | 236 ++++++++++++++++++++++---------------------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/bot/resources/elements.json b/bot/resources/elements.json index 61be9105f..2dc9b6fd6 100644 --- a/bot/resources/elements.json +++ b/bot/resources/elements.json @@ -1,120 +1,120 @@ [ - "Hydrogen", - "Helium", - "Lithium", - "Beryllium", - "Boron", - "Carbon", - "Nitrogen", - "Oxygen", - "Fluorine", - "Neon", - "Sodium", - "Magnesium", - "Aluminium", - "Silicon", - "Phosphorus", - "Sulfur", - "Chlorine", - "Argon", - "Potassium", - "Calcium", - "Scandium", - "Titanium", - "Vanadium", - "Chromium", - "Manganese", - "Iron", - "Cobalt", - "Nickel", - "Copper", - "Zinc", - "Gallium", - "Germanium", - "Arsenic", - "Selenium", - "Bromine", - "Krypton", - "Rubidium", - "Strontium", - "Yttrium", - "Zirconium", - "Niobium", - "Molybdenum", - "Technetium", - "Ruthenium", - "Rhodium", - "Palladium", - "Silver", - "Cadmium", - "Indium", - "Tin", - "Antimony", - "Tellurium", - "Iodine", - "Xenon", - "Caesium", - "Barium", - "Lanthanum", - "Cerium", - "Praseodymium", - "Neodymium", - "Promethium", - "Samarium", - "Europium", - "Gadolinium", - "Terbium", - "Dysprosium", - "Holmium", - "Erbium", - "Thulium", - "Ytterbium", - "Lutetium", - "Hafnium", - "Tantalum", - "Tungsten", - "Rhenium", - "Osmium", - "Iridium", - "Platinum", - "Gold", - "Mercury", - "Thallium", - "Lead", - "Bismuth", - "Polonium", - "Astatine", - "Radon", - "Francium", - "Radium", - "Actinium", - "Thorium", - "Protactinium", - "Uranium", - "Neptunium", - "Plutonium", - "Americium", - "Curium", - "Berkelium", - "Californium", - "Einsteinium", - "Fermium", - "Mendelevium", - "Nobelium", - "Lawrencium", - "Rutherfordium", - "Dubnium", - "Seaborgium", - "Bohrium", - "Hassium", - "Meitnerium", - "Darmstadtium", - "Roentgenium", - "Copernicium", - "Nihonium", - "Flerovium", - "Moscovium", - "Livermorium", - "Tennessine", - "Oganesson" + "hydrogen", + "helium", + "lithium", + "beryllium", + "boron", + "carbon", + "nitrogen", + "oxygen", + "fluorine", + "neon", + "sodium", + "magnesium", + "aluminium", + "silicon", + "phosphorus", + "sulfur", + "chlorine", + "argon", + "potassium", + "calcium", + "scandium", + "titanium", + "vanadium", + "chromium", + "manganese", + "iron", + "cobalt", + "nickel", + "copper", + "zinc", + "gallium", + "germanium", + "arsenic", + "selenium", + "bromine", + "krypton", + "rubidium", + "strontium", + "yttrium", + "zirconium", + "niobium", + "molybdenum", + "technetium", + "ruthenium", + "rhodium", + "palladium", + "silver", + "cadmium", + "indium", + "tin", + "antimony", + "tellurium", + "iodine", + "xenon", + "caesium", + "barium", + "lanthanum", + "cerium", + "praseodymium", + "neodymium", + "promethium", + "samarium", + "europium", + "gadolinium", + "terbium", + "dysprosium", + "holmium", + "erbium", + "thulium", + "ytterbium", + "lutetium", + "hafnium", + "tantalum", + "tungsten", + "rhenium", + "osmium", + "iridium", + "platinum", + "gold", + "mercury", + "thallium", + "lead", + "bismuth", + "polonium", + "astatine", + "radon", + "francium", + "radium", + "actinium", + "thorium", + "protactinium", + "uranium", + "neptunium", + "plutonium", + "americium", + "curium", + "berkelium", + "californium", + "einsteinium", + "fermium", + "mendelevium", + "nobelium", + "lawrencium", + "rutherfordium", + "dubnium", + "seaborgium", + "bohrium", + "hassium", + "meitnerium", + "darmstadtium", + "roentgenium", + "copernicium", + "nihonium", + "flerovium", + "moscovium", + "livermorium", + "tennessine", + "oganesson" ] -- cgit v1.2.3 From 8278d9780a94d44ee779748468dd80689287e91e Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 18:46:14 -0800 Subject: HelpChannels: ignore messages sent by bots --- bot/cogs/help_channels.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 391d400b1..ce71d285a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -344,6 +344,9 @@ class HelpChannels(Scheduler, commands.Cog): @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Move an available channel to the In Use category and replace it with a dormant one.""" + if message.author.bot: + return # Ignore messages sent by bots. + log.trace("Waiting for the cog to be ready before processing messages.") await self.ready.wait() -- cgit v1.2.3 From 2a8e4df7833f876864f4548fe555348a23371c10 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 18:46:25 -0800 Subject: HelpChannels: fix acquisition of the on_message lock * Use async_with * Don't call acquire() --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index ce71d285a..c5c542b3e 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -351,7 +351,7 @@ class HelpChannels(Scheduler, commands.Cog): await self.ready.wait() log.trace("Acquiring lock to prevent a channel from being processed twice...") - with self.on_message_lock.acquire(): + async with self.on_message_lock: log.trace("on_message lock acquired.") log.trace("Checking if the message was sent in an available channel.") -- cgit v1.2.3 From 3926497337e3e65eaa3711f963a447ab32faa811 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 19:22:03 -0800 Subject: HelpChannels: add missing units of time in messages --- bot/cogs/help_channels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index c5c542b3e..7c9bf5e27 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -26,8 +26,8 @@ ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" AVAILABLE_MSG = f""" This help channel is now **available**, which means that you can claim it by simply typing your \ question into it. Once claimed, the channel will move into the **Help: In Use** category, and will \ -be yours until it has been inactive for {constants.HelpChannels.idle_minutes}. When that happens, \ -it will be set to **dormant** and moved into the **Help: Dormant** category. +be yours until it has been inactive for {constants.HelpChannels.idle_minutes} minutes. When that \ +happens, it will be set to **dormant** and moved into the **Help: Dormant** category. Try to write the best question you can by providing a detailed description and telling us what \ you've tried already. For more information on asking a good question, \ @@ -381,7 +381,7 @@ class HelpChannels(Scheduler, commands.Cog): async def _scheduled_task(self, data: ChannelTimeout) -> None: """Make a channel dormant after specified timeout or reschedule if it's still active.""" - log.trace(f"Waiting {data.timeout} before making #{data.channel.name} dormant.") + log.trace(f"Waiting {data.timeout} seconds before making #{data.channel.name} dormant.") await asyncio.sleep(data.timeout) # Use asyncio.shield to prevent move_idle_channel from cancelling itself. -- cgit v1.2.3 From 9af7dbd0f4691918c64a28d5c3d937e81d950289 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 19:22:50 -0800 Subject: HelpChannels: put channels in the queue when they go dormant --- bot/cogs/help_channels.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 7c9bf5e27..86cc5045d 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -324,6 +324,9 @@ class HelpChannels(Scheduler, commands.Cog): embed = discord.Embed(description=DORMANT_MSG) await channel.send(embed=embed) + log.trace(f"Pushing #{channel.name} ({channel.id}) into the channel queue.") + self.channel_queue.put_nowait(channel) + async def move_to_in_use(self, channel: discord.TextChannel) -> None: """Make a channel in-use and schedule it to be made dormant.""" log.info(f"Making #{channel.name} ({channel.id}) in-use.") -- cgit v1.2.3 From 342e5bd532417c48c0120ac4481482f384262b54 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 19:34:56 -0800 Subject: HelpChannels: add a function to get the last message in a channel --- bot/cogs/help_channels.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 86cc5045d..bda6ed7bd 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -191,8 +191,8 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Got {len(names)} used names: {names}") return names - @staticmethod - async def get_idle_time(channel: discord.TextChannel) -> t.Optional[int]: + @classmethod + async def get_idle_time(cls, channel: discord.TextChannel) -> t.Optional[int]: """ Return the time elapsed, in seconds, since the last message sent in the `channel`. @@ -200,9 +200,8 @@ class HelpChannels(Scheduler, commands.Cog): """ log.trace(f"Getting the idle time for #{channel.name} ({channel.id}).") - try: - msg = await channel.history(limit=1).next() # noqa: B305 - except discord.NoMoreItems: + msg = await cls.get_last_message(channel) + if not msg: log.debug(f"No idle time available; #{channel.name} ({channel.id}) has no messages.") return None @@ -211,6 +210,17 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"#{channel.name} ({channel.id}) has been idle for {idle_time} seconds.") return idle_time + @staticmethod + async def get_last_message(channel: discord.TextChannel) -> t.Optional[discord.Message]: + """Return the last message sent in the channel or None if no messages exist.""" + log.trace(f"Getting the last message in #{channel.name} ({channel.id}).") + + try: + return await channel.history(limit=1).next() # noqa: B305 + except discord.NoMoreItems: + log.debug(f"No last message available; #{channel.name} ({channel.id}) has no messages.") + return None + async def init_available(self) -> None: """Initialise the Available category with channels.""" log.trace("Initialising the Available category with channels.") -- cgit v1.2.3 From f605da9b63e8f076296fb75bd5055cc333e46a84 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 19:41:31 -0800 Subject: HelpChannels: add a function to send or edit the available message Edits the dormant message or sends a new message if the dormant one cannot be found. --- bot/cogs/help_channels.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index bda6ed7bd..6c4c6c50e 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -304,13 +304,9 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Making a channel available.") channel = await self.get_available_candidate() - embed = discord.Embed(description=AVAILABLE_MSG) - log.info(f"Making #{channel.name} ({channel.id}) available.") - # TODO: edit or delete the dormant message - log.trace(f"Sending available message for #{channel.name} ({channel.id}).") - await channel.send(embed=embed) + await self.send_available_message(channel) log.trace(f"Moving #{channel.name} ({channel.id}) to the Available category.") await channel.edit( @@ -380,6 +376,21 @@ class HelpChannels(Scheduler, commands.Cog): # be put in the queue. await self.move_to_available() + async def send_available_message(self, channel: discord.TextChannel) -> None: + """Send the available message by editing a dormant message or sending a new message.""" + channel_info = f"#{channel.name} ({channel.id})" + log.trace(f"Sending available message in {channel_info}.") + + embed = discord.Embed(description=AVAILABLE_MSG) + + msg = await self.get_last_message(channel) + if msg: + log.trace(f"Found dormant message {msg.id} in {channel_info}; editing it.") + await msg.edit(embed=embed) + else: + log.trace(f"Dormant message not found in {channel_info}; sending a new message.") + await channel.send(embed=embed) + async def try_get_channel(self, channel_id: int) -> discord.abc.GuildChannel: """Attempt to get or fetch a channel and return it.""" log.trace(f"Getting the channel {channel_id}.") -- cgit v1.2.3 From b88ddd79267fd1a9c4406b81a729e04f514cbcd6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 19:45:14 -0800 Subject: HelpChannels: compare contents to confirm message is a dormant message * Add a new function to check if a message is a dormant message --- bot/cogs/help_channels.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 6c4c6c50e..010acfb34 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -267,6 +267,15 @@ class HelpChannels(Scheduler, commands.Cog): log.info("Cog is ready!") self.ready.set() + @staticmethod + def is_dormant_message(message: t.Optional[discord.Message]) -> bool: + """Return True if the contents of the `message` match `DORMANT_MSG`.""" + if not message or not message.embeds: + return False + + embed = message.embeds[0] + return embed.description.strip() == DORMANT_MSG.strip() + async def move_idle_channel(self, channel: discord.TextChannel) -> None: """ Make the `channel` dormant if idle or schedule the move if still active. @@ -384,7 +393,7 @@ class HelpChannels(Scheduler, commands.Cog): embed = discord.Embed(description=AVAILABLE_MSG) msg = await self.get_last_message(channel) - if msg: + if self.is_dormant_message(msg): log.trace(f"Found dormant message {msg.id} in {channel_info}; editing it.") await msg.edit(embed=embed) else: -- cgit v1.2.3 From ffd3bce5e5af8acd9d681da537fe27ec94201818 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 20:14:02 -0800 Subject: HelpChannels: use >= instead of > to determine if timed out --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 010acfb34..231c34d7b 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -287,7 +287,7 @@ class HelpChannels(Scheduler, commands.Cog): idle_seconds = constants.HelpChannels.idle_minutes * 60 time_elapsed = await self.get_idle_time(channel) - if time_elapsed is None or time_elapsed > idle_seconds: + if time_elapsed is None or time_elapsed >= idle_seconds: log.info( f"#{channel.name} ({channel.id}) is idle longer than {idle_seconds} seconds " f"and will be made dormant." -- cgit v1.2.3 From 1b01d2ea453d9db429704f1265b260d4c8f8fd02 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 20:16:55 -0800 Subject: HelpChannels: cancel the task in _scheduled_task --- bot/cogs/help_channels.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 231c34d7b..394efd3b5 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -421,6 +421,8 @@ class HelpChannels(Scheduler, commands.Cog): # The parent task (_scheduled_task) will still get cancelled. await asyncio.shield(self.move_idle_channel(data.channel)) + self.cancel_task(data.channel.id) + def setup(bot: Bot) -> None: """Load the HelpChannels cog.""" -- cgit v1.2.3 From efb57117032f07cc2a3f2c23c9db6534701728ce Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 24 Feb 2020 20:30:17 -0800 Subject: HelpChannels: explain the system in the cog docstring --- bot/cogs/help_channels.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 394efd3b5..b85fac4f1 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -57,7 +57,36 @@ class ChannelTimeout(t.NamedTuple): class HelpChannels(Scheduler, commands.Cog): - """Manage the help channel system of the guild.""" + """ + Manage the help channel system of the guild. + + The system is based on a 3-category system: + + Available Category + + * Contains channels which are ready to be occupied by someone who needs help + * Will always contain 2 channels; refilled automatically from the pool of dormant channels + * Prioritise using the channels which have been dormant for the longest amount of time + * If there are no more dormant channels, the bot will automatically create a new one + * Configurable with `constants.HelpChannels.max_available` + * When a channel becomes available, the dormant embed will be edited to show `AVAILABLE_MSG` + + In Use Category + + * Contains all channels which are occupied by someone needing help + * Channel moves to dormant category after 45 minutes of being idle + * Configurable with `constants.HelpChannels.idle_minutes` + * Helpers+ command can prematurely mark a channel as dormant + * Configurable with `constants.HelpChannels.cmd_whitelist` + * When a channel becomes dormant, an embed with `DORMANT_MSG` will be sent + + Dormant Category + + * Contains channels which aren't in use + * Channels are used to refill the Available category + + Help channels are named after the chemical elements in `bot/resources/elements.json`. + """ def __init__(self, bot: Bot): super().__init__() -- cgit v1.2.3 From a58e0687845cf8d6aafb559f24b092bc1f4af047 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 27 Feb 2020 09:18:03 -0800 Subject: HelpChannels: limit channels to a total of 50 Discord has a hard limit of 50 channels per category. It was decided 50 is plenty for now so no work will be done to support more than 50. --- bot/cogs/help_channels.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index b85fac4f1..2710e981b 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -46,7 +46,9 @@ through [our guide for asking a good question]({ASKING_GUIDE_URL}). """ with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: - ELEMENTS = json.load(elements_file) + # Discord has a hard limit of 50 channels per category. + # Easiest way to prevent more channels from being created is to limit the names available. + ELEMENTS = json.load(elements_file)[:50] class ChannelTimeout(t.NamedTuple): -- cgit v1.2.3 From 2491a68938f027c7fb08afa14b4130bbdd1da753 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 27 Feb 2020 09:48:24 -0800 Subject: HelpChannels: use more specific type hints for queues --- bot/cogs/help_channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 2710e981b..67bd1ab35 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -99,8 +99,8 @@ class HelpChannels(Scheduler, commands.Cog): self.in_use_category: discord.CategoryChannel = None self.dormant_category: discord.CategoryChannel = None - self.channel_queue: asyncio.Queue = None - self.name_queue: deque = None + self.channel_queue: asyncio.Queue[discord.TextChannel] = None + self.name_queue: t.Deque[str] = None self.ready = asyncio.Event() self.on_message_lock = asyncio.Lock() -- cgit v1.2.3 From c7a0914e8b6a2fffe70b449899ced97fb2619a0c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 27 Feb 2020 19:59:44 -0800 Subject: Resources: map element names to alphabetic indices The indices will be used to sort the elements alphabetically in the dormant category. --- bot/resources/elements.json | 240 ++++++++++++++++++++++---------------------- 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/bot/resources/elements.json b/bot/resources/elements.json index 2dc9b6fd6..bc9047397 100644 --- a/bot/resources/elements.json +++ b/bot/resources/elements.json @@ -1,120 +1,120 @@ -[ - "hydrogen", - "helium", - "lithium", - "beryllium", - "boron", - "carbon", - "nitrogen", - "oxygen", - "fluorine", - "neon", - "sodium", - "magnesium", - "aluminium", - "silicon", - "phosphorus", - "sulfur", - "chlorine", - "argon", - "potassium", - "calcium", - "scandium", - "titanium", - "vanadium", - "chromium", - "manganese", - "iron", - "cobalt", - "nickel", - "copper", - "zinc", - "gallium", - "germanium", - "arsenic", - "selenium", - "bromine", - "krypton", - "rubidium", - "strontium", - "yttrium", - "zirconium", - "niobium", - "molybdenum", - "technetium", - "ruthenium", - "rhodium", - "palladium", - "silver", - "cadmium", - "indium", - "tin", - "antimony", - "tellurium", - "iodine", - "xenon", - "caesium", - "barium", - "lanthanum", - "cerium", - "praseodymium", - "neodymium", - "promethium", - "samarium", - "europium", - "gadolinium", - "terbium", - "dysprosium", - "holmium", - "erbium", - "thulium", - "ytterbium", - "lutetium", - "hafnium", - "tantalum", - "tungsten", - "rhenium", - "osmium", - "iridium", - "platinum", - "gold", - "mercury", - "thallium", - "lead", - "bismuth", - "polonium", - "astatine", - "radon", - "francium", - "radium", - "actinium", - "thorium", - "protactinium", - "uranium", - "neptunium", - "plutonium", - "americium", - "curium", - "berkelium", - "californium", - "einsteinium", - "fermium", - "mendelevium", - "nobelium", - "lawrencium", - "rutherfordium", - "dubnium", - "seaborgium", - "bohrium", - "hassium", - "meitnerium", - "darmstadtium", - "roentgenium", - "copernicium", - "nihonium", - "flerovium", - "moscovium", - "livermorium", - "tennessine", - "oganesson" -] +{ + "hydrogen": 44, + "helium": 42, + "lithium": 53, + "beryllium": 9, + "boron": 12, + "carbon": 18, + "nitrogen": 69, + "oxygen": 73, + "fluorine": 34, + "neon": 64, + "sodium": 97, + "magnesium": 56, + "aluminium": 1, + "silicon": 95, + "phosphorus": 75, + "sulfur": 99, + "chlorine": 20, + "argon": 4, + "potassium": 79, + "calcium": 16, + "scandium": 92, + "titanium": 109, + "vanadium": 112, + "chromium": 21, + "manganese": 57, + "iron": 48, + "cobalt": 22, + "nickel": 66, + "copper": 24, + "zinc": 116, + "gallium": 37, + "germanium": 38, + "arsenic": 5, + "selenium": 94, + "bromine": 13, + "krypton": 49, + "rubidium": 88, + "strontium": 98, + "yttrium": 115, + "zirconium": 117, + "niobium": 68, + "molybdenum": 61, + "technetium": 101, + "ruthenium": 89, + "rhodium": 86, + "palladium": 74, + "silver": 96, + "cadmium": 14, + "indium": 45, + "tin": 108, + "antimony": 3, + "tellurium": 102, + "iodine": 46, + "xenon": 113, + "caesium": 15, + "barium": 7, + "lanthanum": 50, + "cerium": 19, + "praseodymium": 80, + "neodymium": 63, + "promethium": 81, + "samarium": 91, + "europium": 31, + "gadolinium": 36, + "terbium": 104, + "dysprosium": 28, + "holmium": 43, + "erbium": 30, + "thulium": 107, + "ytterbium": 114, + "lutetium": 55, + "hafnium": 40, + "tantalum": 100, + "tungsten": 110, + "rhenium": 85, + "osmium": 72, + "iridium": 47, + "platinum": 76, + "gold": 39, + "mercury": 60, + "thallium": 105, + "lead": 52, + "bismuth": 10, + "polonium": 78, + "astatine": 6, + "radon": 84, + "francium": 35, + "radium": 83, + "actinium": 0, + "thorium": 106, + "protactinium": 82, + "uranium": 111, + "neptunium": 65, + "plutonium": 77, + "americium": 2, + "curium": 25, + "berkelium": 8, + "californium": 17, + "einsteinium": 29, + "fermium": 32, + "mendelevium": 59, + "nobelium": 70, + "lawrencium": 51, + "rutherfordium": 90, + "dubnium": 27, + "seaborgium": 93, + "bohrium": 11, + "hassium": 41, + "meitnerium": 58, + "darmstadtium": 26, + "roentgenium": 87, + "copernicium": 23, + "nihonium": 67, + "flerovium": 33, + "moscovium": 62, + "livermorium": 54, + "tennessine": 103, + "oganesson": 71 +} \ No newline at end of file -- cgit v1.2.3 From 1cd781f787c861790036f02f5c9c0ed3fa1d27cd Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 21:16:02 -0800 Subject: Constants: add constant for max total help channels Represents the total number of help channels across all 3 categories. --- bot/constants.py | 1 + config-default.yml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 5b50050d6..2f484f0ad 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -540,6 +540,7 @@ class HelpChannels(metaclass=YAMLGetter): cmd_whitelist: List[int] idle_minutes: int max_available: int + max_total_channels: int name_prefix: str diff --git a/config-default.yml b/config-default.yml index c095aa30b..a24092235 100644 --- a/config-default.yml +++ b/config-default.yml @@ -525,6 +525,10 @@ help_channels: # Maximum number of channels to put in the available category max_available: 2 + # 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 + max_total_channels: 50 + # Prefix for help channel names name_prefix: 'help-' -- cgit v1.2.3 From 37ec93d8eaf255ff9c118469d8a86231c9427680 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 07:51:24 -0800 Subject: HelpChannels: move reading of element names to a function Makes it easier to test. --- bot/cogs/help_channels.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 67bd1ab35..0c6c48914 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -45,11 +45,6 @@ question to maximize your chance of getting a good answer. If you're not sure ho through [our guide for asking a good question]({ASKING_GUIDE_URL}). """ -with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: - # Discord has a hard limit of 50 channels per category. - # Easiest way to prevent more channels from being created is to limit the names available. - ELEMENTS = json.load(elements_file)[:50] - class ChannelTimeout(t.NamedTuple): """Data for a task scheduled to make a channel dormant.""" @@ -102,6 +97,8 @@ class HelpChannels(Scheduler, commands.Cog): self.channel_queue: asyncio.Queue[discord.TextChannel] = None self.name_queue: t.Deque[str] = None + self.elements = self.get_names() + self.ready = asyncio.Event() self.on_message_lock = asyncio.Lock() self.init_task = self.bot.loop.create_task(self.init_cog()) @@ -160,7 +157,7 @@ class HelpChannels(Scheduler, commands.Cog): used_names = self.get_used_names() log.trace("Determining the available names.") - available_names = (name for name in ELEMENTS if name not in used_names) + available_names = (name for name in self.elements if name not in used_names) log.trace("Populating the name queue with names.") return deque(available_names) @@ -207,6 +204,14 @@ class HelpChannels(Scheduler, commands.Cog): if channel.category_id == category.id and isinstance(channel, discord.TextChannel): yield channel + @staticmethod + def get_names() -> t.List[str]: + """Return a list of element names.""" + with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: + # Discord has a hard limit of 50 channels per category. + # Easiest way to prevent more channels from being created is to limit available names. + return json.load(elements_file)[:50] + def get_used_names(self) -> t.Set[str]: """Return channels names which are already being used.""" log.trace("Getting channel names which are already being used.") -- cgit v1.2.3 From a3f4f3d19b6ba82b7bbb2e2bf01416c5fd1c0f31 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 07:58:35 -0800 Subject: HelpChannels: return elements as a truncated dict of names --- bot/cogs/help_channels.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 0c6c48914..c8609f168 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,4 +1,5 @@ import asyncio +import itertools import json import logging import random @@ -205,12 +206,13 @@ class HelpChannels(Scheduler, commands.Cog): yield channel @staticmethod - def get_names() -> t.List[str]: - """Return a list of element names.""" + def get_names(count: int = constants.HelpChannels.max_total_channels) -> t.Dict[str, int]: + """Return a dict with the first `count` element names and their alphabetical indices.""" with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: - # Discord has a hard limit of 50 channels per category. - # Easiest way to prevent more channels from being created is to limit available names. - return json.load(elements_file)[:50] + all_names = json.load(elements_file) + + truncated_names = itertools.islice(all_names.items(), count) + return dict(truncated_names) def get_used_names(self) -> t.Set[str]: """Return channels names which are already being used.""" -- cgit v1.2.3 From 379f4093b1ea2e8f403046512486a0d962ab697d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 08:04:01 -0800 Subject: HelpChannels: warn if too many help channels will be possible Discord only supports 50 channels per category. * Add a constant for the maximum number of channels per category * Add trace logging to `get_names` --- bot/cogs/help_channels.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index c8609f168..64443f81c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -23,6 +23,7 @@ AVAILABLE_TOPIC = "" IN_USE_TOPIC = "" DORMANT_TOPIC = "" ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" +MAX_CHANNELS_PER_CATEGORY = 50 AVAILABLE_MSG = f""" This help channel is now **available**, which means that you can claim it by simply typing your \ @@ -208,6 +209,14 @@ class HelpChannels(Scheduler, commands.Cog): @staticmethod def get_names(count: int = constants.HelpChannels.max_total_channels) -> t.Dict[str, int]: """Return a dict with the first `count` element names and their alphabetical indices.""" + log.trace(f"Getting the first {count} element names from JSON.") + + if count > MAX_CHANNELS_PER_CATEGORY: + log.warning( + f"{count} is too many help channels to make available! " + f"Discord only supports at most {MAX_CHANNELS_PER_CATEGORY} channels per category." + ) + with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: all_names = json.load(elements_file) -- cgit v1.2.3 From 9597921a8a11e3e915e5fe49e7a1ace46a81b98f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 27 Feb 2020 20:16:30 -0800 Subject: HelpChannels: sort dormant channels alphabetically The channels are easier to find when sorted alphabetically. * Merge some trace and info logs --- bot/cogs/help_channels.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 64443f81c..0f700a9ba 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -373,13 +373,16 @@ class HelpChannels(Scheduler, commands.Cog): async def move_to_dormant(self, channel: discord.TextChannel) -> None: """Make the `channel` dormant.""" - log.info(f"Making #{channel.name} ({channel.id}) dormant.") + log.info(f"Moving #{channel.name} ({channel.id}) to the Dormant category.") + + start_index = len(constants.HelpChannels.name_prefix) + element = channel.name[start_index:] - log.trace(f"Moving #{channel.name} ({channel.id}) to the Dormant category.") await channel.edit( category=self.dormant_category, sync_permissions=True, topic=DORMANT_TOPIC, + position=self.elements[element], ) log.trace(f"Sending dormant message for #{channel.name} ({channel.id}).") @@ -391,9 +394,8 @@ class HelpChannels(Scheduler, commands.Cog): async def move_to_in_use(self, channel: discord.TextChannel) -> None: """Make a channel in-use and schedule it to be made dormant.""" - log.info(f"Making #{channel.name} ({channel.id}) in-use.") + log.info(f"Moving #{channel.name} ({channel.id}) to the In Use category.") - log.trace(f"Moving #{channel.name} ({channel.id}) to the In Use category.") await channel.edit( category=self.in_use_category, sync_permissions=True, -- cgit v1.2.3 From 05ea5a932c0e8941d22e64f3a616772876816018 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 27 Feb 2020 20:20:06 -0800 Subject: HelpChannels: add a warning if more than 50 channels exist Discord only supports 50 channels per category. The help system will eventually error out trying to move channels if more than 50 exist. --- bot/cogs/help_channels.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 0f700a9ba..4aad55cfe 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -235,6 +235,12 @@ class HelpChannels(Scheduler, commands.Cog): name = channel.name[start_index:] names.add(name) + if len(names) > MAX_CHANNELS_PER_CATEGORY: + log.warning( + f"Too many help channels ({len(names)}) already exist! " + f"Discord only supports {MAX_CHANNELS_PER_CATEGORY} in a category." + ) + log.trace(f"Got {len(names)} used names: {names}") return names -- cgit v1.2.3 From a0eb4e1872426f4b6675f9e08204bf295dbaa09f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 27 Feb 2020 21:20:44 -0800 Subject: HelpChannels: add a function to get a channel's alphabetical position * Log a warning if a channel lacks the expected help channel prefix * Log the old and new channel positions --- bot/cogs/help_channels.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 4aad55cfe..bb2103c3a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -196,6 +196,26 @@ class HelpChannels(Scheduler, commands.Cog): return channel + def get_alphabetical_position(self, channel: discord.TextChannel) -> t.Optional[int]: + """ + Return the position to move `channel` to so alphabetic order is maintained. + + If the channel does not have a valid name with a chemical element, return None. + """ + log.trace(f"Getting alphabetical position for #{channel.name} ({channel.id}).") + + prefix = constants.HelpChannels.name_prefix + element = channel.name[len(prefix):] + + try: + position = self.elements[element] + except KeyError: + log.warning(f"Channel #{channel.name} ({channel.id}) doesn't have the prefix {prefix}.") + position = None + + log.trace(f"Position of #{channel.name} ({channel.id}) in Dormant will be {position}.") + return position + @staticmethod def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: """Yield the text channels of the `category` in an unsorted manner.""" @@ -381,16 +401,15 @@ class HelpChannels(Scheduler, commands.Cog): """Make the `channel` dormant.""" log.info(f"Moving #{channel.name} ({channel.id}) to the Dormant category.") - start_index = len(constants.HelpChannels.name_prefix) - element = channel.name[start_index:] - await channel.edit( category=self.dormant_category, sync_permissions=True, topic=DORMANT_TOPIC, - position=self.elements[element], + position=self.get_alphabetical_position(channel), ) + log.trace(f"Position of #{channel.name} ({channel.id}) is actually {channel.position}.") + log.trace(f"Sending dormant message for #{channel.name} ({channel.id}).") embed = discord.Embed(description=DORMANT_MSG) await channel.send(embed=embed) -- cgit v1.2.3 From bcad1ad3a06df3a0d350839f2d861882f05dcd84 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 27 Feb 2020 22:13:43 -0800 Subject: HelpChannels: notify helpers if out of channels Send a message in the #helpers channel pinging the @helpers role to notify them of a lack of help channels. Can be toggled off in the config. --- bot/cogs/help_channels.py | 9 +++++++++ bot/constants.py | 1 + config-default.yml | 3 +++ 3 files changed, 13 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index bb2103c3a..f9fe9e6de 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -192,6 +192,15 @@ class HelpChannels(Scheduler, commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") + + if constants.HelpChannels.notify_helpers: + helpers_channel = self.bot.get_channel(constants.Channels.helpers) + await helpers_channel.send( + f"<@&{constants.Roles.helpers}> a help channel is in needed but none are " + f"available. Consider freeing up some in-use channels manually by using " + f"the `!dormant` command within the channels." + ) + channel = await self.channel_queue.get() return channel diff --git a/bot/constants.py b/bot/constants.py index 2f484f0ad..56ab7f84c 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -542,6 +542,7 @@ class HelpChannels(metaclass=YAMLGetter): max_available: int max_total_channels: int name_prefix: str + notify_helpers: bool class Mention(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index a24092235..0c4e226aa 100644 --- a/config-default.yml +++ b/config-default.yml @@ -532,6 +532,9 @@ help_channels: # Prefix for help channel names name_prefix: 'help-' + # Notify helpers if more channels are needed but none are available + notify_helpers: true + redirect_output: delete_invocation: true delete_delay: 15 -- cgit v1.2.3 From f07221b8135eb067490f51ee79832893cd4ba610 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 07:28:19 -0800 Subject: HelpChannels: log previous position when getting alphabetical position --- bot/cogs/help_channels.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index f9fe9e6de..85015b5e9 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -222,7 +222,11 @@ class HelpChannels(Scheduler, commands.Cog): log.warning(f"Channel #{channel.name} ({channel.id}) doesn't have the prefix {prefix}.") position = None - log.trace(f"Position of #{channel.name} ({channel.id}) in Dormant will be {position}.") + log.trace( + f"Position of #{channel.name} ({channel.id}) in Dormant will be {position} " + f"(was {channel.position})." + ) + return position @staticmethod -- cgit v1.2.3 From f7e29ba78ec3f25965ae9fb70729cc3eb895e808 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 07:43:40 -0800 Subject: HelpChannels: add a minimum interval between helper notifications * Add configurable constant for minimum interval * Move helper notifications to a separate function --- bot/cogs/help_channels.py | 37 ++++++++++++++++++++++++++++--------- bot/constants.py | 1 + config-default.yml | 3 +++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 85015b5e9..809551131 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -100,6 +100,7 @@ class HelpChannels(Scheduler, commands.Cog): self.name_queue: t.Deque[str] = None self.elements = self.get_names() + self.last_notification: t.Optional[datetime] = None self.ready = asyncio.Event() self.on_message_lock = asyncio.Lock() @@ -192,15 +193,7 @@ class HelpChannels(Scheduler, commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") - - if constants.HelpChannels.notify_helpers: - helpers_channel = self.bot.get_channel(constants.Channels.helpers) - await helpers_channel.send( - f"<@&{constants.Roles.helpers}> a help channel is in needed but none are " - f"available. Consider freeing up some in-use channels manually by using " - f"the `!dormant` command within the channels." - ) - + await self.notify_helpers() channel = await self.channel_queue.get() return channel @@ -446,6 +439,32 @@ class HelpChannels(Scheduler, commands.Cog): data = ChannelTimeout(channel, timeout) self.schedule_task(self.bot.loop, channel.id, data) + async def notify_helpers(self) -> None: + """ + Notify helpers in the #helpers channel about a lack of available help channels. + + The notification can be disabled with `constants.HelpChannels.notify_helpers`. The + minimum interval between notifications can be configured with + `constants.HelpChannels.notify_minutes`. + """ + if not constants.HelpChannels.notify_helpers: + return + + if self.last_notification: + elapsed = (datetime.utcnow() - self.last_notification).seconds + minimum_interval = constants.HelpChannels.notify_minutes * 60 + should_send = elapsed >= minimum_interval + else: + should_send = True + + if should_send: + helpers_channel = self.bot.get_channel(constants.Channels.helpers) + await helpers_channel.send( + f"<@&{constants.Roles.helpers}> a help channel is in needed but none are " + f"available. Consider freeing up some in-use channels manually by using " + f"the `!dormant` command within the channels." + ) + @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: """Move an available channel to the In Use category and replace it with a dormant one.""" diff --git a/bot/constants.py b/bot/constants.py index 56ab7f84c..00694e1d7 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -543,6 +543,7 @@ class HelpChannels(metaclass=YAMLGetter): max_total_channels: int name_prefix: str notify_helpers: bool + notify_minutes: int class Mention(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index 0c4e226aa..764cf5a3e 100644 --- a/config-default.yml +++ b/config-default.yml @@ -535,6 +535,9 @@ help_channels: # Notify helpers if more channels are needed but none are available notify_helpers: true + # Minimum interval between helper notifications + notify_minutes: 15 + redirect_output: delete_invocation: true delete_delay: 15 -- cgit v1.2.3 From f01400c2516add585b8dcc90417e0273b3f87cde Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 07:45:02 -0800 Subject: HelpChannels: adjust the helper notification message --- bot/cogs/help_channels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 809551131..f63370b24 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -460,9 +460,9 @@ class HelpChannels(Scheduler, commands.Cog): if should_send: helpers_channel = self.bot.get_channel(constants.Channels.helpers) await helpers_channel.send( - f"<@&{constants.Roles.helpers}> a help channel is in needed but none are " - f"available. Consider freeing up some in-use channels manually by using " - f"the `!dormant` command within the channels." + f"<@&{constants.Roles.helpers}> a new available help channel is needed but there " + f"are no more dormant ones. Consider freeing up some in-use channels manually by " + f"using the `!dormant` command within the channels." ) @commands.Cog.listener() -- cgit v1.2.3 From 52b89b4cc406d0f9346e28b5ceb9bc8712be59b8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 08:01:00 -0800 Subject: Constants: rename HelpChannels.notify_helpers to notify --- bot/cogs/help_channels.py | 2 +- bot/constants.py | 2 +- config-default.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index f63370b24..dde52e1e7 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -447,7 +447,7 @@ class HelpChannels(Scheduler, commands.Cog): minimum interval between notifications can be configured with `constants.HelpChannels.notify_minutes`. """ - if not constants.HelpChannels.notify_helpers: + if not constants.HelpChannels.notify: return if self.last_notification: diff --git a/bot/constants.py b/bot/constants.py index 00694e1d7..32968cfaa 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -542,7 +542,7 @@ class HelpChannels(metaclass=YAMLGetter): max_available: int max_total_channels: int name_prefix: str - notify_helpers: bool + notify: bool notify_minutes: int diff --git a/config-default.yml b/config-default.yml index 764cf5a3e..27dfbdc64 100644 --- a/config-default.yml +++ b/config-default.yml @@ -532,8 +532,8 @@ help_channels: # Prefix for help channel names name_prefix: 'help-' - # Notify helpers if more channels are needed but none are available - notify_helpers: true + # Notify if more available channels are needed but there are no more dormant ones + notify: true # Minimum interval between helper notifications notify_minutes: 15 -- cgit v1.2.3 From 77392c9522eb2f69657d41537ba4e2109c35c315 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 08:05:23 -0800 Subject: Constants: add a channel constant for help channel notifications --- bot/cogs/help_channels.py | 12 ++++++------ bot/constants.py | 1 + config-default.yml | 4 ++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index dde52e1e7..d50a21f0b 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -441,11 +441,11 @@ class HelpChannels(Scheduler, commands.Cog): async def notify_helpers(self) -> None: """ - Notify helpers in the #helpers channel about a lack of available help channels. + Notify helpers about a lack of available help channels. - The notification can be disabled with `constants.HelpChannels.notify_helpers`. The - minimum interval between notifications can be configured with - `constants.HelpChannels.notify_minutes`. + The notification can be disabled with `constants.HelpChannels.notify`. The minimum interval + between notifications can be configured with `constants.HelpChannels.notify_minutes`. The + channel for notifications can be configured with `constants.HelpChannels.notify_channel`. """ if not constants.HelpChannels.notify: return @@ -458,8 +458,8 @@ class HelpChannels(Scheduler, commands.Cog): should_send = True if should_send: - helpers_channel = self.bot.get_channel(constants.Channels.helpers) - await helpers_channel.send( + channel = self.bot.get_channel(constants.HelpChannels.notify_channel) + await channel.send( f"<@&{constants.Roles.helpers}> a new available help channel is needed but there " f"are no more dormant ones. Consider freeing up some in-use channels manually by " f"using the `!dormant` command within the channels." diff --git a/bot/constants.py b/bot/constants.py index 32968cfaa..7a0760eef 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -543,6 +543,7 @@ class HelpChannels(metaclass=YAMLGetter): max_total_channels: int name_prefix: str notify: bool + notify_channel: int notify_minutes: int diff --git a/config-default.yml b/config-default.yml index 27dfbdc64..b23271899 100644 --- a/config-default.yml +++ b/config-default.yml @@ -532,9 +532,13 @@ help_channels: # Prefix for help channel names name_prefix: 'help-' + # Notify if more available channels are needed but there are no more dormant ones notify: true + # Channel in which to send notifications + notify_channel: *HELPERS + # Minimum interval between helper notifications notify_minutes: 15 -- cgit v1.2.3 From 91fd12b0b61bfe731095d30450ac5a24f7ff948c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 08:07:01 -0800 Subject: Constants: add a roles list constant for help channel notifications --- bot/constants.py | 1 + config-default.yml | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/constants.py b/bot/constants.py index 7a0760eef..042e48a8b 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -545,6 +545,7 @@ class HelpChannels(metaclass=YAMLGetter): notify: bool notify_channel: int notify_minutes: int + notify_roles: List[int] class Mention(metaclass=YAMLGetter): diff --git a/config-default.yml b/config-default.yml index b23271899..22c05853c 100644 --- a/config-default.yml +++ b/config-default.yml @@ -532,7 +532,6 @@ help_channels: # Prefix for help channel names name_prefix: 'help-' - # Notify if more available channels are needed but there are no more dormant ones notify: true @@ -542,6 +541,10 @@ help_channels: # Minimum interval between helper notifications notify_minutes: 15 + # Mention these roles in notifications + notify_roles: + - *HELPERS_ROLE + redirect_output: delete_invocation: true delete_delay: 15 -- cgit v1.2.3 From 8ffa5930214d53adc18554b982d6556815e5bd97 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 08:28:42 -0800 Subject: HelpChannels: notify configured list of roles instead of helpers only * Rename function `notify_helpers` -> `notify` * Use bullet-point list for config options in the docstring --- bot/cogs/help_channels.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index d50a21f0b..28bbac790 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -193,7 +193,7 @@ class HelpChannels(Scheduler, commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") - await self.notify_helpers() + await self.notify() channel = await self.channel_queue.get() return channel @@ -439,13 +439,16 @@ class HelpChannels(Scheduler, commands.Cog): data = ChannelTimeout(channel, timeout) self.schedule_task(self.bot.loop, channel.id, data) - async def notify_helpers(self) -> None: + async def notify(self) -> None: """ - Notify helpers about a lack of available help channels. + Send a message notifying about a lack of available help channels. - The notification can be disabled with `constants.HelpChannels.notify`. The minimum interval - between notifications can be configured with `constants.HelpChannels.notify_minutes`. The - channel for notifications can be configured with `constants.HelpChannels.notify_channel`. + Configuration: + + * `HelpChannels.notify` - toggle notifications + * `HelpChannels.notify_channel` - destination channel for notifications + * `HelpChannels.notify_minutes` - minimum interval between notifications + * `HelpChannels.notify_roles` - roles mentioned in notifications """ if not constants.HelpChannels.notify: return @@ -459,8 +462,10 @@ class HelpChannels(Scheduler, commands.Cog): if should_send: channel = self.bot.get_channel(constants.HelpChannels.notify_channel) + mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_roles) + await channel.send( - f"<@&{constants.Roles.helpers}> a new available help channel is needed but there " + f"{mentions} A new available help channel is needed but there " f"are no more dormant ones. Consider freeing up some in-use channels manually by " f"using the `!dormant` command within the channels." ) -- cgit v1.2.3 From cc5753e48d5ed5c3f2b73f8a724ab34b446cb5ef Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 08:33:15 -0800 Subject: HelpChannels: handle potential notification exceptions locally The notification feature is not critical for the functionality of the help channel system. Therefore, the exception should not be allowed to propagate and halt the system in some way. --- bot/cogs/help_channels.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 28bbac790..45509e1c7 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -460,7 +460,10 @@ class HelpChannels(Scheduler, commands.Cog): else: should_send = True - if should_send: + if not should_send: + return + + try: channel = self.bot.get_channel(constants.HelpChannels.notify_channel) mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_roles) @@ -469,6 +472,9 @@ class HelpChannels(Scheduler, commands.Cog): f"are no more dormant ones. Consider freeing up some in-use channels manually by " f"using the `!dormant` command within the channels." ) + except Exception: + # Handle it here cause this feature isn't critical for the functionality of the system. + log.exception("Failed to send notification about lack of dormant channels!") @commands.Cog.listener() async def on_message(self, message: discord.Message) -> None: -- cgit v1.2.3 From deef254e905bf55ebadf36e0e2a5e1616a796706 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 11:04:51 -0800 Subject: Constants: remove old help channel constants --- bot/constants.py | 8 -------- config-default.yml | 10 ---------- 2 files changed, 18 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 042e48a8b..7f19f8a0e 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -376,14 +376,6 @@ class Channels(metaclass=YAMLGetter): dev_core: int dev_log: int esoteric: int - help_0: int - help_1: int - help_2: int - help_3: int - help_4: int - help_5: int - help_6: int - help_7: int helpers: int message_log: int meta: int diff --git a/config-default.yml b/config-default.yml index 22c05853c..b91df4580 100644 --- a/config-default.yml +++ b/config-default.yml @@ -140,16 +140,6 @@ guild: off_topic_1: 463035241142026251 off_topic_2: 463035268514185226 - # Python Help - help_0: 303906576991780866 - help_1: 303906556754395136 - help_2: 303906514266226689 - help_3: 439702951246692352 - help_4: 451312046647148554 - help_5: 454941769734422538 - help_6: 587375753306570782 - help_7: 587375768556797982 - # Special bot_commands: &BOT_CMD 267659945086812160 esoteric: 470884583684964352 -- cgit v1.2.3 From d1fe7eb011c9d124173045efc408f5b022a0c356 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 11:06:53 -0800 Subject: BotCog: determine if a help channel by checking its category Initialising the dictionary with help channel IDs doesn't work anymore since help channels are now dynamic. --- bot/cogs/bot.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py index f17135877..267892cc3 100644 --- a/bot/cogs/bot.py +++ b/bot/cogs/bot.py @@ -9,7 +9,7 @@ from discord.ext.commands import Cog, Context, command, group from bot.bot import Bot from bot.cogs.token_remover import TokenRemover -from bot.constants import Channels, DEBUG_MODE, Guild, MODERATION_ROLES, Roles, URLs +from bot.constants import Categories, Channels, DEBUG_MODE, Guild, MODERATION_ROLES, Roles, URLs from bot.decorators import with_role from bot.utils.messages import wait_for_deletion @@ -26,14 +26,6 @@ class BotCog(Cog, name="Bot"): # Stores allowed channels plus epoch time since last call. self.channel_cooldowns = { - Channels.help_0: 0, - Channels.help_1: 0, - Channels.help_2: 0, - Channels.help_3: 0, - Channels.help_4: 0, - Channels.help_5: 0, - Channels.help_6: 0, - Channels.help_7: 0, Channels.python_discussion: 0, } @@ -232,9 +224,14 @@ class BotCog(Cog, name="Bot"): If poorly formatted code is detected, send the user a helpful message explaining how to do properly formatted Python syntax highlighting codeblocks. """ + is_help_channel = ( + msg.channel.category + and msg.channel.category.id in (Categories.help_available, Categories.help_in_use) + ) parse_codeblock = ( ( - msg.channel.id in self.channel_cooldowns + is_help_channel + or msg.channel.id in self.channel_cooldowns or msg.channel.id in self.channel_whitelist ) and not msg.author.bot -- cgit v1.2.3 From 5a9661106186ab370b31aa2858210f121a944846 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 11:15:38 -0800 Subject: HelpChannels: move newest in-use channel to the top This gives the newest questions the most visibility. --- bot/cogs/help_channels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 45509e1c7..ab2f5b8c3 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -431,6 +431,7 @@ class HelpChannels(Scheduler, commands.Cog): category=self.in_use_category, sync_permissions=True, topic=IN_USE_TOPIC, + position=0, ) timeout = constants.HelpChannels.idle_minutes * 60 -- cgit v1.2.3 From e42f4a7d6487fa76cb41abad2dda2f7e2e5b7c4d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 11:29:27 -0800 Subject: HelpChannels: add trace logging for notifications --- bot/cogs/help_channels.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index ab2f5b8c3..d226b201a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -454,6 +454,8 @@ class HelpChannels(Scheduler, commands.Cog): if not constants.HelpChannels.notify: return + log.trace("Notifying about lack of channels.") + if self.last_notification: elapsed = (datetime.utcnow() - self.last_notification).seconds minimum_interval = constants.HelpChannels.notify_minutes * 60 @@ -462,9 +464,12 @@ class HelpChannels(Scheduler, commands.Cog): should_send = True if not should_send: + log.trace("Notification not sent because it's too recent since the previous one.") return try: + log.trace("Sending notification message.") + channel = self.bot.get_channel(constants.HelpChannels.notify_channel) mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_roles) -- cgit v1.2.3 From fc5bda29a0cb6beee931d48a83bc5f093a22499c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 28 Feb 2020 11:35:47 -0800 Subject: HelpChannels: cancel channel queue tasks on cog unload * Store queue get() tasks in a list * Create a separate function to wait for a channel from the queue * Add comments for the various groups of attributes defined in __init__ --- bot/cogs/help_channels.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index d226b201a..6bed3199c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -92,16 +92,20 @@ class HelpChannels(Scheduler, commands.Cog): self.bot = bot + # Categories self.available_category: discord.CategoryChannel = None self.in_use_category: discord.CategoryChannel = None self.dormant_category: discord.CategoryChannel = None + # Queues self.channel_queue: asyncio.Queue[discord.TextChannel] = None self.name_queue: t.Deque[str] = None self.elements = self.get_names() self.last_notification: t.Optional[datetime] = None + # Asyncio stuff + self.queue_tasks: t.List[asyncio.Task] = [] self.ready = asyncio.Event() self.on_message_lock = asyncio.Lock() self.init_task = self.bot.loop.create_task(self.init_cog()) @@ -111,6 +115,10 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Cog unload: cancelling the cog_init task") self.init_task.cancel() + log.trace("Cog unload: cancelling the channel queue tasks") + for task in self.queue_tasks: + task.cancel() + log.trace("Cog unload: cancelling the scheduled tasks") for task in self.scheduled_tasks.values(): task.cancel() @@ -194,7 +202,7 @@ class HelpChannels(Scheduler, commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") await self.notify() - channel = await self.channel_queue.get() + await self.wait_for_dormant_channel() return channel @@ -535,6 +543,19 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Channel #{channel.name} ({channel_id}) retrieved.") return channel + async def wait_for_dormant_channel(self) -> discord.TextChannel: + """Wait for a dormant channel to become available in the queue and return it.""" + log.trace("Waiting for a dormant channel.") + + task = asyncio.create_task(self.channel_queue.get()) + self.queue_tasks.append(task) + channel = await task + + log.trace(f"Channel #{channel.name} ({channel.id}) finally retrieved from the queue.") + self.queue_tasks.remove(task) + + return channel + async def _scheduled_task(self, data: ChannelTimeout) -> None: """Make a channel dormant after specified timeout or reschedule if it's still active.""" log.trace(f"Waiting {data.timeout} seconds before making #{data.channel.name} dormant.") -- cgit v1.2.3 From 5becd8bffd299153d0d36685857aafa8ffb3a8e2 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 08:53:20 -0800 Subject: HelpChannels: prefix channel names after reading from file Prefixing them early on means subsequent code doesn't have to deal with stripping the prefix from channel names in order to get their positions. * Remove `count` parameter from `get_names`; define it in the body --- bot/cogs/help_channels.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 6bed3199c..d0ebce7a4 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -150,10 +150,9 @@ class HelpChannels(Scheduler, commands.Cog): Return None if no more channel names are available. """ log.trace("Getting a name for a new dormant channel.") - name = constants.HelpChannels.name_prefix try: - name += self.name_queue.popleft() + name = self.name_queue.popleft() except IndexError: log.debug("No more names available for new dormant channels.") return None @@ -214,13 +213,10 @@ class HelpChannels(Scheduler, commands.Cog): """ log.trace(f"Getting alphabetical position for #{channel.name} ({channel.id}).") - prefix = constants.HelpChannels.name_prefix - element = channel.name[len(prefix):] - try: - position = self.elements[element] + position = self.elements[channel.name] except KeyError: - log.warning(f"Channel #{channel.name} ({channel.id}) doesn't have the prefix {prefix}.") + log.warning(f"Channel #{channel.name} ({channel.id}) doesn't have a valid name.") position = None log.trace( @@ -241,8 +237,16 @@ class HelpChannels(Scheduler, commands.Cog): yield channel @staticmethod - def get_names(count: int = constants.HelpChannels.max_total_channels) -> t.Dict[str, int]: - """Return a dict with the first `count` element names and their alphabetical indices.""" + def get_names() -> t.Dict[str, int]: + """ + Return a truncated dict of prefixed element names and their alphabetical indices. + + The amount of names if configured with `HelpChannels.max_total_channels`. + The prefix is configured with `HelpChannels.name_prefix`. + """ + count = constants.HelpChannels.max_total_channels + prefix = constants.HelpChannels.name_prefix + log.trace(f"Getting the first {count} element names from JSON.") if count > MAX_CHANNELS_PER_CATEGORY: @@ -254,20 +258,20 @@ class HelpChannels(Scheduler, commands.Cog): with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: all_names = json.load(elements_file) - truncated_names = itertools.islice(all_names.items(), count) - return dict(truncated_names) + names = itertools.islice(all_names.items(), count) + if prefix: + names = ((prefix + name, pos) for name, pos in names) + + return dict(names) def get_used_names(self) -> t.Set[str]: """Return channels names which are already being used.""" log.trace("Getting channel names which are already being used.") - start_index = len(constants.HelpChannels.name_prefix) - names = set() for cat in (self.available_category, self.in_use_category, self.dormant_category): for channel in self.get_category_channels(cat): - name = channel.name[start_index:] - names.add(name) + names.add(channel.name) if len(names) > MAX_CHANNELS_PER_CATEGORY: log.warning( -- cgit v1.2.3 From 578850d483644c79ee3124dbf13ee43e234ac78a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 08:54:04 -0800 Subject: HelpChannels: rename elements dict to name_positions --- bot/cogs/help_channels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index d0ebce7a4..4c83e0722 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -101,7 +101,7 @@ class HelpChannels(Scheduler, commands.Cog): self.channel_queue: asyncio.Queue[discord.TextChannel] = None self.name_queue: t.Deque[str] = None - self.elements = self.get_names() + self.name_positions = self.get_names() self.last_notification: t.Optional[datetime] = None # Asyncio stuff @@ -167,7 +167,7 @@ class HelpChannels(Scheduler, commands.Cog): used_names = self.get_used_names() log.trace("Determining the available names.") - available_names = (name for name in self.elements if name not in used_names) + available_names = (name for name in self.name_positions if name not in used_names) log.trace("Populating the name queue with names.") return deque(available_names) @@ -214,7 +214,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Getting alphabetical position for #{channel.name} ({channel.id}).") try: - position = self.elements[channel.name] + position = self.name_positions[channel.name] except KeyError: log.warning(f"Channel #{channel.name} ({channel.id}) doesn't have a valid name.") position = None -- cgit v1.2.3 From 746673dbadb1a8b9872eeeeb21155214b846bba3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 09:12:58 -0800 Subject: Scheduler: add a method to cancel all tasks The dictionary which was iterated to cancel tasks is now "private". Therefore, the scheduler should provide a public API for cancelling tasks. * Delete the task before cancelling it to prevent the done callback, however unlikely it may be, from deleting the task first --- bot/cogs/help_channels.py | 4 +--- bot/utils/scheduling.py | 9 ++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 4c83e0722..01bcc28f7 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -119,9 +119,7 @@ class HelpChannels(Scheduler, commands.Cog): for task in self.queue_tasks: task.cancel() - log.trace("Cog unload: cancelling the scheduled tasks") - for task in self.scheduled_tasks.values(): - task.cancel() + self.cancel_all() def create_channel_queue(self) -> asyncio.Queue: """ diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py index 5760ec2d4..e9a9e6c2d 100644 --- a/bot/utils/scheduling.py +++ b/bot/utils/scheduling.py @@ -60,11 +60,18 @@ class Scheduler(metaclass=CogABCMeta): log.warning(f"{self.cog_name}: failed to unschedule {task_id} (no task found).") return - task.cancel() del self._scheduled_tasks[task_id] + task.cancel() log.debug(f"{self.cog_name}: unscheduled task #{task_id} {id(task)}.") + def cancel_all(self) -> None: + """Unschedule all known tasks.""" + log.debug(f"{self.cog_name}: unscheduling all tasks") + + for task_id in self._scheduled_tasks: + self.cancel_task(task_id) + def _task_done_callback(self, task_id: t.Hashable, done_task: asyncio.Task) -> None: """ Delete the task and raise its exception if one exists. -- cgit v1.2.3 From 13d4c82ffafe8ba6cd0a6dab061bef02547d66b1 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 09:30:16 -0800 Subject: HelpChannels: explicitly specify if a task should be cancelled When rescheduling an idle channel, the task will only be cancelled if the function was told the channel should currently have a task scheduled. This means warnings for missing tasks will appear when they should. The previous approach of checking if a task exists was flawed because it had no way to tell whether a task *should* exist. It assumed nothing is wrong if a task doesn't exist. Currently, the only case when a task shouldn't exist is when the cog is initialised and channels from the bot's previous life are being scheduled. --- bot/cogs/help_channels.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 01bcc28f7..12c64c39b 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -351,7 +351,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Moving or rescheduling in-use channels.") for channel in self.get_category_channels(self.in_use_category): - await self.move_idle_channel(channel) + await self.move_idle_channel(channel, has_task=False) log.info("Cog is ready!") self.ready.set() @@ -365,11 +365,12 @@ class HelpChannels(Scheduler, commands.Cog): embed = message.embeds[0] return embed.description.strip() == DORMANT_MSG.strip() - async def move_idle_channel(self, channel: discord.TextChannel) -> None: + async def move_idle_channel(self, channel: discord.TextChannel, has_task: bool = True) -> None: """ Make the `channel` dormant if idle or schedule the move if still active. - If a task to make the channel dormant already exists, it will first be cancelled. + If `has_task` is True and rescheduling is required, the extant task to make the channel + dormant will first be cancelled. """ log.trace(f"Handling in-use channel #{channel.name} ({channel.id}).") @@ -385,7 +386,7 @@ class HelpChannels(Scheduler, commands.Cog): await self.move_to_dormant(channel) else: # Cancel the existing task, if any. - if channel.id in self.scheduled_tasks: + if has_task: self.cancel_task(channel.id) data = ChannelTimeout(channel, idle_seconds - time_elapsed) -- cgit v1.2.3 From 2e7ac620027870ecbf21d3ff5e78553f96449eee Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 09:30:37 -0800 Subject: HelpChannels: remove loop arg from schedule_task calls --- bot/cogs/help_channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 12c64c39b..fc944999f 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -396,7 +396,7 @@ class HelpChannels(Scheduler, commands.Cog): f"scheduling it to be moved after {data.timeout} seconds." ) - self.schedule_task(self.bot.loop, channel.id, data) + self.schedule_task(channel.id, data) async def move_to_available(self) -> None: """Make a channel available.""" @@ -449,7 +449,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Scheduling #{channel.name} ({channel.id}) to become dormant in {timeout} sec.") data = ChannelTimeout(channel, timeout) - self.schedule_task(self.bot.loop, channel.id, data) + self.schedule_task(channel.id, data) async def notify(self) -> None: """ -- cgit v1.2.3 From 0f36d442afc9633263d2a7f9a7d5f09aa6a5ac86 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 09:39:24 -0800 Subject: HelpChannels: fix candidate channel not being returned after waiting --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index fc944999f..8da07e9f7 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -199,7 +199,7 @@ class HelpChannels(Scheduler, commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") await self.notify() - await self.wait_for_dormant_channel() + channel = await self.wait_for_dormant_channel() return channel -- cgit v1.2.3 From 0a5e6662fc65f1c097d5daca48d4856fa4839a58 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 09:44:22 -0800 Subject: HelpChannels: cancel existing task in the dormant command --- bot/cogs/help_channels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 8da07e9f7..dfc9e0119 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -178,6 +178,7 @@ class HelpChannels(Scheduler, commands.Cog): in_use = self.get_category_channels(self.in_use_category) if ctx.channel in in_use: + self.cancel_task(ctx.channel.id) await self.move_to_dormant(ctx.channel) else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") -- cgit v1.2.3 From 2225bfe97adf9547fc5817878778dcb7c62b2dd4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 09:53:00 -0800 Subject: Scheduler: fix incorrect task id in error log --- bot/utils/scheduling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py index e9a9e6c2d..f8b9d2d48 100644 --- a/bot/utils/scheduling.py +++ b/bot/utils/scheduling.py @@ -105,6 +105,6 @@ class Scheduler(metaclass=CogABCMeta): # Log the exception if one exists. if exception: log.error( - f"{self.cog_name}: error in task #{task_id} {id(scheduled_task)}!", + f"{self.cog_name}: error in task #{task_id} {id(done_task)}!", exc_info=exception ) -- cgit v1.2.3 From 12eac1690e310b87be32295858b12410276ff153 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 10:18:08 -0800 Subject: HelpChannels: fix task cancelling itself --- bot/cogs/help_channels.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index dfc9e0119..0e3b1e893 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -569,8 +569,6 @@ class HelpChannels(Scheduler, commands.Cog): # The parent task (_scheduled_task) will still get cancelled. await asyncio.shield(self.move_idle_channel(data.channel)) - self.cancel_task(data.channel.id) - def setup(bot: Bot) -> None: """Load the HelpChannels cog.""" -- cgit v1.2.3 From 825da2a9d24b04ea8e319e0bd7972465934ba6b5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 10:20:57 -0800 Subject: HelpChannels: fix last notification time not being set --- bot/cogs/help_channels.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 0e3b1e893..b215ed3d7 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -485,11 +485,13 @@ class HelpChannels(Scheduler, commands.Cog): channel = self.bot.get_channel(constants.HelpChannels.notify_channel) mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_roles) - await channel.send( + message = await channel.send( f"{mentions} A new available help channel is needed but there " f"are no more dormant ones. Consider freeing up some in-use channels manually by " f"using the `!dormant` command within the channels." ) + + self.last_notification = message.created_at except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") -- cgit v1.2.3 From 92df4ba80ba85fb9735d8ab6867da4f72e5ed87a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 10:31:02 -0800 Subject: Scheduler: fix dict size changing while cancelling all tasks * Make a copy of the dict * Add a `ignore_missing` param to `cancel_task` to suppress the warning for a task not being found --- bot/utils/scheduling.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py index f8b9d2d48..8b778a093 100644 --- a/bot/utils/scheduling.py +++ b/bot/utils/scheduling.py @@ -51,13 +51,18 @@ class Scheduler(metaclass=CogABCMeta): self._scheduled_tasks[task_id] = task log.debug(f"{self.cog_name}: scheduled task #{task_id} {id(task)}.") - def cancel_task(self, task_id: t.Hashable) -> None: - """Unschedule the task identified by `task_id`.""" + def cancel_task(self, task_id: t.Hashable, ignore_missing: bool = False) -> None: + """ + Unschedule the task identified by `task_id`. + + If `ignore_missing` is True, a warning will not be sent if a task isn't found. + """ log.trace(f"{self.cog_name}: cancelling task #{task_id}...") task = self._scheduled_tasks.get(task_id) if not task: - log.warning(f"{self.cog_name}: failed to unschedule {task_id} (no task found).") + if not ignore_missing: + log.warning(f"{self.cog_name}: failed to unschedule {task_id} (no task found).") return del self._scheduled_tasks[task_id] @@ -69,8 +74,8 @@ class Scheduler(metaclass=CogABCMeta): """Unschedule all known tasks.""" log.debug(f"{self.cog_name}: unscheduling all tasks") - for task_id in self._scheduled_tasks: - self.cancel_task(task_id) + for task_id in self._scheduled_tasks.copy(): + self.cancel_task(task_id, ignore_missing=True) def _task_done_callback(self, task_id: t.Hashable, done_task: asyncio.Task) -> None: """ -- cgit v1.2.3 From 90ddb10c9d02870b5cd6ad4b491d04665865cb9d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 10:49:51 -0800 Subject: HelpChannels: disable the dormant command until cog is ready The ready event wasn't used because channels could change categories between the time the command is invoked and the cog is ready (e.g. if move_idle_channel wasn't called yet). his may confused users. So would potentially long delays for the cog to become ready. --- bot/cogs/help_channels.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index b215ed3d7..d769b2619 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -112,7 +112,7 @@ class HelpChannels(Scheduler, commands.Cog): def cog_unload(self) -> None: """Cancel the init task and scheduled tasks when the cog unloads.""" - log.trace("Cog unload: cancelling the cog_init task") + log.trace("Cog unload: cancelling the init_cog task") self.init_task.cancel() log.trace("Cog unload: cancelling the channel queue tasks") @@ -170,7 +170,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Populating the name queue with names.") return deque(available_names) - @commands.command(name="dormant") + @commands.command(name="dormant", enabled=False) @with_role(*constants.HelpChannels.cmd_whitelist) async def dormant_command(self, ctx: commands.Context) -> None: """Make the current in-use help channel dormant.""" @@ -354,6 +354,12 @@ class HelpChannels(Scheduler, commands.Cog): for channel in self.get_category_channels(self.in_use_category): await self.move_idle_channel(channel, has_task=False) + # Prevent the command from being used until ready. + # The ready event wasn't used because channels could change categories between the time + # the command is invoked and the cog is ready (e.g. if move_idle_channel wasn't called yet). + # This may confused users. So would potentially long delays for the cog to become ready. + self.dormant_command.enabled = True + log.info("Cog is ready!") self.ready.set() -- cgit v1.2.3 From 6774bb93836c2264d015ff9b9625dae6a91b1d56 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 11:08:07 -0800 Subject: HelpChannels: initialise available channels after moving idle ones This will ensure the maximum amount of dormant channels possible before attempting to move any to the available category. It also allows the dormant command to already be enabled in case there are still no dormant channels when trying to init available channels. --- bot/cogs/help_channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index d769b2619..2fd5cc851 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -348,8 +348,6 @@ class HelpChannels(Scheduler, commands.Cog): self.channel_queue = self.create_channel_queue() self.name_queue = self.create_name_queue() - await self.init_available() - log.trace("Moving or rescheduling in-use channels.") for channel in self.get_category_channels(self.in_use_category): await self.move_idle_channel(channel, has_task=False) @@ -360,6 +358,8 @@ class HelpChannels(Scheduler, commands.Cog): # This may confused users. So would potentially long delays for the cog to become ready. self.dormant_command.enabled = True + await self.init_available() + log.info("Cog is ready!") self.ready.set() -- cgit v1.2.3 From 4a87de480c2dc7b23df8ab1d17e39c8c09bcef9f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 11:53:28 -0800 Subject: HelpChannels: prevent cog load if config is invalid They must be greater than 0 because the cog obviously couldn't do anything without any channels to work with. It must be greater than max_available because it'd otherwise be impossible to maintain that many channels in the Available category. * Create a new function to validate the value * Move validation against MAX_CHANNELS_PER_CATEGORY into the function rather than just logging a warning --- bot/cogs/help_channels.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 2fd5cc851..314eefa00 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -248,12 +248,6 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Getting the first {count} element names from JSON.") - if count > MAX_CHANNELS_PER_CATEGORY: - log.warning( - f"{count} is too many help channels to make available! " - f"Discord only supports at most {MAX_CHANNELS_PER_CATEGORY} channels per category." - ) - with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: all_names = json.load(elements_file) @@ -578,6 +572,33 @@ class HelpChannels(Scheduler, commands.Cog): await asyncio.shield(self.move_idle_channel(data.channel)) +def validate_config() -> None: + """Raise a ValueError if the cog's config is invalid.""" + log.trace("Validating config.") + total = constants.HelpChannels.max_total_channels + available = constants.HelpChannels.max_available + + if total == 0 or available == 0: + raise ValueError("max_total_channels and max_available and must be greater than 0.") + + if total < available: + raise ValueError( + f"max_total_channels ({total}) must be greater than or equal to max_available " + f"({available})." + ) + + if total > MAX_CHANNELS_PER_CATEGORY: + raise ValueError( + f"max_total_channels ({total}) must be less than or equal to " + f"{MAX_CHANNELS_PER_CATEGORY} due to Discord's limit on channels per category." + ) + + def setup(bot: Bot) -> None: """Load the HelpChannels cog.""" - bot.add_cog(HelpChannels(bot)) + try: + validate_config() + except ValueError as e: + log.error(f"HelpChannels cog will not be loaded due to misconfiguration: {e}") + else: + bot.add_cog(HelpChannels(bot)) -- cgit v1.2.3 From 4d43b63a8c72594ac52cfd8f7e51e2504cf81ce0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 12:26:12 -0800 Subject: HelpChannels: create generic way to schedule any awaitable To support scheduling different coroutines, `_scheduled_task` now accepts an awaitable in the data arg. The data arg is actually a named tuple of the wait time and the awaitable. --- bot/cogs/help_channels.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 314eefa00..5b8de156e 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -48,11 +48,11 @@ through [our guide for asking a good question]({ASKING_GUIDE_URL}). """ -class ChannelTimeout(t.NamedTuple): - """Data for a task scheduled to make a channel dormant.""" +class TaskData(t.NamedTuple): + """Data for a scheduled task.""" - channel: discord.TextChannel - timeout: int + wait_time: int + callback: t.Awaitable class HelpChannels(Scheduler, commands.Cog): @@ -390,11 +390,11 @@ class HelpChannels(Scheduler, commands.Cog): if has_task: self.cancel_task(channel.id) - data = ChannelTimeout(channel, idle_seconds - time_elapsed) + data = TaskData(idle_seconds - time_elapsed, self.move_idle_channel(channel)) log.info( f"#{channel.name} ({channel.id}) is still active; " - f"scheduling it to be moved after {data.timeout} seconds." + f"scheduling it to be moved after {data.wait_time} seconds." ) self.schedule_task(channel.id, data) @@ -449,7 +449,7 @@ class HelpChannels(Scheduler, commands.Cog): timeout = constants.HelpChannels.idle_minutes * 60 log.trace(f"Scheduling #{channel.name} ({channel.id}) to become dormant in {timeout} sec.") - data = ChannelTimeout(channel, timeout) + data = TaskData(timeout, self.move_idle_channel(channel)) self.schedule_task(channel.id, data) async def notify(self) -> None: @@ -562,14 +562,14 @@ class HelpChannels(Scheduler, commands.Cog): return channel - async def _scheduled_task(self, data: ChannelTimeout) -> None: - """Make a channel dormant after specified timeout or reschedule if it's still active.""" - log.trace(f"Waiting {data.timeout} seconds before making #{data.channel.name} dormant.") - await asyncio.sleep(data.timeout) + async def _scheduled_task(self, data: TaskData) -> None: + """Await the `data.callback` coroutine after waiting for `data.wait_time` seconds.""" + log.trace(f"Waiting {data.wait_time} seconds before awaiting callback.") + await asyncio.sleep(data.wait_time) - # Use asyncio.shield to prevent move_idle_channel from cancelling itself. + # Use asyncio.shield to prevent callback from cancelling itself. # The parent task (_scheduled_task) will still get cancelled. - await asyncio.shield(self.move_idle_channel(data.channel)) + await asyncio.shield(data.callback) def validate_config() -> None: -- cgit v1.2.3 From 6173072011b69c54fe817f383b70487e0ad97dee Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 12:37:30 -0800 Subject: HelpChannels: allow users to claim a new channel every 15 minutes --- bot/cogs/help_channels.py | 20 ++++++++++++++++++++ bot/constants.py | 1 + config-default.yml | 3 +++ 3 files changed, 24 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 5b8de156e..e6401b14b 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -515,6 +515,7 @@ class HelpChannels(Scheduler, commands.Cog): return # Ignore messages outside the Available category. await self.move_to_in_use(message.channel) + await self.revoke_send_permissions(message.author) log.trace("Releasing on_message lock.") # Move a dormant channel to the Available category to fill in the gap. @@ -522,6 +523,25 @@ class HelpChannels(Scheduler, commands.Cog): # be put in the queue. await self.move_to_available() + async def revoke_send_permissions(self, member: discord.Member) -> None: + """ + Disallow `member` to send messages in the Available category for a certain time. + + The time until permissions are reinstated can be configured with + `HelpChannels.claim_minutes`. + """ + log.trace( + f"Revoking {member}'s ({member.id}) send message permissions in the Available category." + ) + + await self.available_category.set_permissions(member, send_messages=False) + + timeout = constants.HelpChannels.claim_minutes * 60 + callback = self.available_category.set_permissions(member, send_messages=None) + + log.trace(f"Scheduling {member}'s ({member.id}) send message permissions to be reinstated.") + self.schedule_task(member.id, TaskData(timeout, callback)) + async def send_available_message(self, channel: discord.TextChannel) -> None: """Send the available message by editing a dormant message or sending a new message.""" channel_info = f"#{channel.name} ({channel.id})" diff --git a/bot/constants.py b/bot/constants.py index 7f19f8a0e..8e9d40e8d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -529,6 +529,7 @@ class Free(metaclass=YAMLGetter): class HelpChannels(metaclass=YAMLGetter): section = 'help_channels' + claim_minutes: int cmd_whitelist: List[int] idle_minutes: int max_available: int diff --git a/config-default.yml b/config-default.yml index b91df4580..a62572b70 100644 --- a/config-default.yml +++ b/config-default.yml @@ -505,6 +505,9 @@ mention: reset_delay: 5 help_channels: + # Minimum interval before allowing a certain user to claim a new help channel + claim_minutes: 15 + # Roles which are allowed to use the command which makes channels dormant cmd_whitelist: - *HELPERS_ROLE -- cgit v1.2.3 From d29afb8c710b20a46475546e4dd0cd950c51ab5f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 12:45:42 -0800 Subject: HelpChannels: reset send permissions This ensures everyone has a clean slate when the bot restarts or the cog reloads since the tasks to reinstate permissions would have been cancelled in those cases. --- bot/cogs/help_channels.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index e6401b14b..ee8eb2e1c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -338,6 +338,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Initialising the cog.") await self.init_categories() + await self.reset_send_permissions() self.channel_queue = self.create_channel_queue() self.name_queue = self.create_name_queue() @@ -523,6 +524,15 @@ class HelpChannels(Scheduler, commands.Cog): # be put in the queue. await self.move_to_available() + async def reset_send_permissions(self) -> None: + """Reset send permissions for members with it set to False in the Available category.""" + log.trace("Resetting send permissions in the Available category.") + + for member, overwrite in self.available_category.overwrites.items(): + if isinstance(member, discord.Member) and overwrite.send_messages is False: + log.trace(f"Resetting send permissions for {member} ({member.id}).") + await self.available_category.set_permissions(member, send_messages=None) + async def revoke_send_permissions(self, member: discord.Member) -> None: """ Disallow `member` to send messages in the Available category for a certain time. -- cgit v1.2.3 From ca5415629e3721a31568089eae362226eb07f561 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 12:54:53 -0800 Subject: HelpChannels: include info about claim cooldowns in available message --- bot/cogs/help_channels.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index ee8eb2e1c..8dd17e936 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -31,6 +31,9 @@ question into it. Once claimed, the channel will move into the **Help: In Use** be yours until it has been inactive for {constants.HelpChannels.idle_minutes} minutes. When that \ happens, it will be set to **dormant** and moved into the **Help: Dormant** category. +You may claim a new channel once every {constants.HelpChannels.claim_minutes} minutes. If you \ +currently cannot send a message in this channel, it means you are on cooldown and need to wait. + Try to write the best question you can by providing a detailed description and telling us what \ you've tried already. For more information on asking a good question, \ [check out our guide on asking good questions]({ASKING_GUIDE_URL}). -- cgit v1.2.3 From b020e375a00ed2924ccd9be964326326a3737d4f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 14:09:39 -0800 Subject: HelpChannels: make category checks direct & efficient Replace retrieval of all channels of a category with a direct comparison of the categories themselves. In the case of the `on_message` listener, the change enables the check to be done before the lock acquisition. This is because it doesn't rely on the channels in the category to be up to date. In fact, it doesn't even need the category object so it can exit early without needing to wait for the cog to be ready. --- bot/cogs/help_channels.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 8dd17e936..6ed66b80a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -179,8 +179,7 @@ class HelpChannels(Scheduler, commands.Cog): """Make the current in-use help channel dormant.""" log.trace("dormant command invoked; checking if the channel is in-use.") - in_use = self.get_category_channels(self.in_use_category) - if ctx.channel in in_use: + if ctx.channel.category == self.in_use_category: self.cancel_task(ctx.channel.id) await self.move_to_dormant(ctx.channel) else: @@ -506,21 +505,28 @@ class HelpChannels(Scheduler, commands.Cog): if message.author.bot: return # Ignore messages sent by bots. + channel = message.channel + if channel.category and channel.category.id != constants.Categories.help_available: + return # Ignore messages outside the Available category. + log.trace("Waiting for the cog to be ready before processing messages.") await self.ready.wait() log.trace("Acquiring lock to prevent a channel from being processed twice...") async with self.on_message_lock: - log.trace("on_message lock acquired.") - log.trace("Checking if the message was sent in an available channel.") + log.trace(f"on_message lock acquired for {message.id}.") - available_channels = self.get_category_channels(self.available_category) - if message.channel not in available_channels: - return # Ignore messages outside the Available category. + if channel.category and channel.category.id != constants.Categories.help_available: + log.debug( + f"Message {message.id} will not make #{channel} ({channel.id}) in-use " + f"because another message in the channel already triggered that." + ) + return - await self.move_to_in_use(message.channel) + await self.move_to_in_use(channel) await self.revoke_send_permissions(message.author) - log.trace("Releasing on_message lock.") + + log.trace(f"Releasing on_message lock for {message.id}.") # Move a dormant channel to the Available category to fill in the gap. # This is done last and outside the lock because it may wait indefinitely for a channel to -- cgit v1.2.3 From 940b82c14186215083b7b37e6b06a525b9fbd924 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 14:12:29 -0800 Subject: HelpChannels: remove name attribute access for channels in logs Can rely on `__str__` already being a channel's name. --- bot/cogs/help_channels.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 6ed66b80a..5dbc40b6a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -212,16 +212,16 @@ class HelpChannels(Scheduler, commands.Cog): If the channel does not have a valid name with a chemical element, return None. """ - log.trace(f"Getting alphabetical position for #{channel.name} ({channel.id}).") + log.trace(f"Getting alphabetical position for #{channel} ({channel.id}).") try: position = self.name_positions[channel.name] except KeyError: - log.warning(f"Channel #{channel.name} ({channel.id}) doesn't have a valid name.") + log.warning(f"Channel #{channel} ({channel.id}) doesn't have a valid name.") position = None log.trace( - f"Position of #{channel.name} ({channel.id}) in Dormant will be {position} " + f"Position of #{channel} ({channel.id}) in Dormant will be {position} " f"(was {channel.position})." ) @@ -230,7 +230,7 @@ class HelpChannels(Scheduler, commands.Cog): @staticmethod def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: """Yield the text channels of the `category` in an unsorted manner.""" - log.trace(f"Getting text channels in the category '{category.name}' ({category.id}).") + log.trace(f"Getting text channels in the category '{category}' ({category.id}).") # This is faster than using category.channels because the latter sorts them. for channel in category.guild.channels: @@ -284,27 +284,27 @@ class HelpChannels(Scheduler, commands.Cog): Return None if the channel has no messages. """ - log.trace(f"Getting the idle time for #{channel.name} ({channel.id}).") + log.trace(f"Getting the idle time for #{channel} ({channel.id}).") msg = await cls.get_last_message(channel) if not msg: - log.debug(f"No idle time available; #{channel.name} ({channel.id}) has no messages.") + log.debug(f"No idle time available; #{channel} ({channel.id}) has no messages.") return None idle_time = (datetime.utcnow() - msg.created_at).seconds - log.trace(f"#{channel.name} ({channel.id}) has been idle for {idle_time} seconds.") + log.trace(f"#{channel} ({channel.id}) has been idle for {idle_time} seconds.") return idle_time @staticmethod async def get_last_message(channel: discord.TextChannel) -> t.Optional[discord.Message]: """Return the last message sent in the channel or None if no messages exist.""" - log.trace(f"Getting the last message in #{channel.name} ({channel.id}).") + log.trace(f"Getting the last message in #{channel} ({channel.id}).") try: return await channel.history(limit=1).next() # noqa: B305 except discord.NoMoreItems: - log.debug(f"No last message available; #{channel.name} ({channel.id}) has no messages.") + log.debug(f"No last message available; #{channel} ({channel.id}) has no messages.") return None async def init_available(self) -> None: @@ -376,14 +376,14 @@ class HelpChannels(Scheduler, commands.Cog): If `has_task` is True and rescheduling is required, the extant task to make the channel dormant will first be cancelled. """ - log.trace(f"Handling in-use channel #{channel.name} ({channel.id}).") + log.trace(f"Handling in-use channel #{channel} ({channel.id}).") idle_seconds = constants.HelpChannels.idle_minutes * 60 time_elapsed = await self.get_idle_time(channel) if time_elapsed is None or time_elapsed >= idle_seconds: log.info( - f"#{channel.name} ({channel.id}) is idle longer than {idle_seconds} seconds " + f"#{channel} ({channel.id}) is idle longer than {idle_seconds} seconds " f"and will be made dormant." ) @@ -396,7 +396,7 @@ class HelpChannels(Scheduler, commands.Cog): data = TaskData(idle_seconds - time_elapsed, self.move_idle_channel(channel)) log.info( - f"#{channel.name} ({channel.id}) is still active; " + f"#{channel} ({channel.id}) is still active; " f"scheduling it to be moved after {data.wait_time} seconds." ) @@ -407,11 +407,11 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Making a channel available.") channel = await self.get_available_candidate() - log.info(f"Making #{channel.name} ({channel.id}) available.") + log.info(f"Making #{channel} ({channel.id}) available.") await self.send_available_message(channel) - log.trace(f"Moving #{channel.name} ({channel.id}) to the Available category.") + log.trace(f"Moving #{channel} ({channel.id}) to the Available category.") await channel.edit( category=self.available_category, sync_permissions=True, @@ -420,7 +420,7 @@ class HelpChannels(Scheduler, commands.Cog): async def move_to_dormant(self, channel: discord.TextChannel) -> None: """Make the `channel` dormant.""" - log.info(f"Moving #{channel.name} ({channel.id}) to the Dormant category.") + log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") await channel.edit( category=self.dormant_category, @@ -429,18 +429,18 @@ class HelpChannels(Scheduler, commands.Cog): position=self.get_alphabetical_position(channel), ) - log.trace(f"Position of #{channel.name} ({channel.id}) is actually {channel.position}.") + log.trace(f"Position of #{channel} ({channel.id}) is actually {channel.position}.") - log.trace(f"Sending dormant message for #{channel.name} ({channel.id}).") + log.trace(f"Sending dormant message for #{channel} ({channel.id}).") embed = discord.Embed(description=DORMANT_MSG) await channel.send(embed=embed) - log.trace(f"Pushing #{channel.name} ({channel.id}) into the channel queue.") + log.trace(f"Pushing #{channel} ({channel.id}) into the channel queue.") self.channel_queue.put_nowait(channel) async def move_to_in_use(self, channel: discord.TextChannel) -> None: """Make a channel in-use and schedule it to be made dormant.""" - log.info(f"Moving #{channel.name} ({channel.id}) to the In Use category.") + log.info(f"Moving #{channel} ({channel.id}) to the In Use category.") await channel.edit( category=self.in_use_category, @@ -451,7 +451,7 @@ class HelpChannels(Scheduler, commands.Cog): timeout = constants.HelpChannels.idle_minutes * 60 - log.trace(f"Scheduling #{channel.name} ({channel.id}) to become dormant in {timeout} sec.") + log.trace(f"Scheduling #{channel} ({channel.id}) to become dormant in {timeout} sec.") data = TaskData(timeout, self.move_idle_channel(channel)) self.schedule_task(channel.id, data) @@ -563,7 +563,7 @@ class HelpChannels(Scheduler, commands.Cog): async def send_available_message(self, channel: discord.TextChannel) -> None: """Send the available message by editing a dormant message or sending a new message.""" - channel_info = f"#{channel.name} ({channel.id})" + channel_info = f"#{channel} ({channel.id})" log.trace(f"Sending available message in {channel_info}.") embed = discord.Embed(description=AVAILABLE_MSG) @@ -585,7 +585,7 @@ class HelpChannels(Scheduler, commands.Cog): log.debug(f"Channel {channel_id} is not in cache; fetching from API.") channel = await self.bot.fetch_channel(channel_id) - log.trace(f"Channel #{channel.name} ({channel_id}) retrieved.") + log.trace(f"Channel #{channel} ({channel_id}) retrieved.") return channel async def wait_for_dormant_channel(self) -> discord.TextChannel: @@ -596,7 +596,7 @@ class HelpChannels(Scheduler, commands.Cog): self.queue_tasks.append(task) channel = await task - log.trace(f"Channel #{channel.name} ({channel.id}) finally retrieved from the queue.") + log.trace(f"Channel #{channel} ({channel.id}) finally retrieved from the queue.") self.queue_tasks.remove(task) return channel -- cgit v1.2.3 From 249d13e9d25b4ab4a1207c65aa9c0ee59e55b733 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 15:33:54 -0800 Subject: HelpChannels: fix unawaited coro warning Explicitly close the callback if it's a coroutine. --- bot/cogs/help_channels.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 5dbc40b6a..d6031d7ff 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,4 +1,5 @@ import asyncio +import inspect import itertools import json import logging @@ -603,12 +604,18 @@ class HelpChannels(Scheduler, commands.Cog): async def _scheduled_task(self, data: TaskData) -> None: """Await the `data.callback` coroutine after waiting for `data.wait_time` seconds.""" - log.trace(f"Waiting {data.wait_time} seconds before awaiting callback.") - await asyncio.sleep(data.wait_time) - - # Use asyncio.shield to prevent callback from cancelling itself. - # The parent task (_scheduled_task) will still get cancelled. - await asyncio.shield(data.callback) + try: + log.trace(f"Waiting {data.wait_time} seconds before awaiting callback.") + await asyncio.sleep(data.wait_time) + + # Use asyncio.shield to prevent callback from cancelling itself. + # The parent task (_scheduled_task) will still get cancelled. + log.trace("Done waiting; now awaiting the callback.") + await asyncio.shield(data.callback) + finally: + if inspect.iscoroutine(data.callback): + log.trace("Explicitly closing coroutine.") + data.callback.close() def validate_config() -> None: -- cgit v1.2.3 From a3f67c5361f1acb8f4aa022b7a59209c2f175412 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 29 Feb 2020 15:45:23 -0800 Subject: HelpChannels: fix unawaited coro warning for set_permissions This was happening when attempting to schedule a task twice for a user. Because the scheduler refuses to schedule a duplicate, the coroutine is deallocated right away without being awaited (or closed explicitly by `scheduled_task`). To fix, any existing task is cancelled before scheduling. This also means if somehow a user bypasses the lack of permissions, their cooldown will be updated. However, it probably doesn't make a difference as if they can bypass once, they likely can bypass again. --- bot/cogs/help_channels.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index d6031d7ff..6725efb72 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -556,6 +556,10 @@ class HelpChannels(Scheduler, commands.Cog): await self.available_category.set_permissions(member, send_messages=False) + # Cancel the existing task, if any. + # Would mean the user somehow bypassed the lack of permissions (e.g. user is guild owner). + self.cancel_task(member.id, ignore_missing=True) + timeout = constants.HelpChannels.claim_minutes * 60 callback = self.available_category.set_permissions(member, send_messages=None) -- cgit v1.2.3 From aa5b6e9609ee00928d1591124eb92748879072df Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 1 Mar 2020 13:18:18 -0800 Subject: HelpChannels: use constant for command prefix in notification --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 6725efb72..7d793c73a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -492,7 +492,7 @@ class HelpChannels(Scheduler, commands.Cog): message = await channel.send( f"{mentions} A new available help channel is needed but there " f"are no more dormant ones. Consider freeing up some in-use channels manually by " - f"using the `!dormant` command within the channels." + f"using the `{constants.Bot.prefix}dormant` command within the channels." ) self.last_notification = message.created_at -- cgit v1.2.3 From 9b1e1efc2a63945b173c380bf3dcda17d69be089 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 10 Mar 2020 13:03:45 -0700 Subject: HelpChannels: remove permission overwrites completely Resetting a specific permission still keeps the overwrite for the member around despite having default values. These will accumulate over time so they should be completely removed once the permission needs to be reset. --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 7d793c73a..973e6369a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -561,7 +561,7 @@ class HelpChannels(Scheduler, commands.Cog): self.cancel_task(member.id, ignore_missing=True) timeout = constants.HelpChannels.claim_minutes * 60 - callback = self.available_category.set_permissions(member, send_messages=None) + callback = self.available_category.set_permissions(member, overwrite=None) log.trace(f"Scheduling {member}'s ({member.id}) send message permissions to be reinstated.") self.schedule_task(member.id, TaskData(timeout, callback)) -- cgit v1.2.3 From 943eddbee5b79bf848f76c6138e90b973d8ea0ce Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 10 Mar 2020 13:19:03 -0700 Subject: Resources: add newline to end of elements.json --- bot/resources/elements.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/elements.json b/bot/resources/elements.json index bc9047397..6ea4964aa 100644 --- a/bot/resources/elements.json +++ b/bot/resources/elements.json @@ -117,4 +117,4 @@ "livermorium": 54, "tennessine": 103, "oganesson": 71 -} \ No newline at end of file +} -- cgit v1.2.3 From 19b31434115d54f814f5d17a34a647c27cce536c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 22 Mar 2020 14:40:21 -0700 Subject: HelpChannels: write channel topics --- bot/cogs/help_channels.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 973e6369a..7f169fe59 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -19,13 +19,23 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) -# TODO: write the channel topics -AVAILABLE_TOPIC = "" -IN_USE_TOPIC = "" -DORMANT_TOPIC = "" ASKING_GUIDE_URL = "https://pythondiscord.com/pages/asking-good-questions/" MAX_CHANNELS_PER_CATEGORY = 50 +AVAILABLE_TOPIC = """ +This channel is available. Feel free to ask a question in order to claim this channel! +""" + +IN_USE_TOPIC = """ +This channel is currently in use. If you'd like to discuss a different problem, please claim a new \ +channel from the Help: Available category. +""" + +DORMANT_TOPIC = """ +This channel is temporarily archived. If you'd like to ask a question, please use one of the \ +channels in the Help: Available category. +""" + AVAILABLE_MSG = f""" This help channel is now **available**, which means that you can claim it by simply typing your \ question into it. Once claimed, the channel will move into the **Help: In Use** category, and will \ -- cgit v1.2.3 From 5fba7ab0ca1bde23f028b654d0a69d0d48bec211 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 22 Mar 2020 14:43:59 -0700 Subject: HelpChannels: set idle minutes to 30 & max total channels to 32 --- config-default.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-default.yml b/config-default.yml index a62572b70..d87ceade7 100644 --- a/config-default.yml +++ b/config-default.yml @@ -513,14 +513,14 @@ help_channels: - *HELPERS_ROLE # Allowed duration of inactivity before making a channel dormant - idle_minutes: 45 + idle_minutes: 30 # Maximum number of channels to put in the available category max_available: 2 # 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 - max_total_channels: 50 + max_total_channels: 32 # Prefix for help channel names name_prefix: 'help-' -- cgit v1.2.3 From 5cfeec42aca39b3b6e6ec0739c71085daa987aca Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 22 Mar 2020 14:52:56 -0700 Subject: HelpChannels: mention the helper notifications in cog docstring --- bot/cogs/help_channels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 7f169fe59..69af085ee 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -82,6 +82,7 @@ class HelpChannels(Scheduler, commands.Cog): * Prioritise using the channels which have been dormant for the longest amount of time * If there are no more dormant channels, the bot will automatically create a new one * Configurable with `constants.HelpChannels.max_available` + * If there are no dormant channels to move, helpers will be notified (see `notify()`) * When a channel becomes available, the dormant embed will be edited to show `AVAILABLE_MSG` In Use Category -- cgit v1.2.3 From ece898460abc2937e9b73d53d2f1f695069ef2e1 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 22 Mar 2020 15:03:34 -0700 Subject: Constants: add a config value to toggle help channels extension --- bot/__main__.py | 13 +++++++------ bot/constants.py | 1 + config-default.yml | 2 ++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bot/__main__.py b/bot/__main__.py index 30a7dee41..f6ca5a9c8 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -5,9 +5,8 @@ import sentry_sdk from discord.ext.commands import when_mentioned_or from sentry_sdk.integrations.logging import LoggingIntegration -from bot import patches +from bot import constants, patches from bot.bot import Bot -from bot.constants import Bot as BotConfig sentry_logging = LoggingIntegration( level=logging.DEBUG, @@ -15,12 +14,12 @@ sentry_logging = LoggingIntegration( ) sentry_sdk.init( - dsn=BotConfig.sentry_dsn, + dsn=constants.Bot.sentry_dsn, integrations=[sentry_logging] ) bot = Bot( - command_prefix=when_mentioned_or(BotConfig.prefix), + command_prefix=when_mentioned_or(constants.Bot.prefix), activity=discord.Game(name="Commands: !help"), case_insensitive=True, max_messages=10_000, @@ -49,7 +48,6 @@ bot.load_extension("bot.cogs.alias") bot.load_extension("bot.cogs.defcon") bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.duck_pond") -bot.load_extension("bot.cogs.help_channels") bot.load_extension("bot.cogs.information") bot.load_extension("bot.cogs.jams") bot.load_extension("bot.cogs.moderation") @@ -65,8 +63,11 @@ bot.load_extension("bot.cogs.utils") bot.load_extension("bot.cogs.watchchannels") bot.load_extension("bot.cogs.wolfram") +if constants.HelpChannels.enable: + bot.load_extension("bot.cogs.help_channels") + # Apply `message_edited_at` patch if discord.py did not yet release a bug fix. if not hasattr(discord.message.Message, '_handle_edited_timestamp'): patches.message_edited_at.apply_patch() -bot.run(BotConfig.token) +bot.run(constants.Bot.token) diff --git a/bot/constants.py b/bot/constants.py index 8e9d40e8d..da1a62780 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -529,6 +529,7 @@ class Free(metaclass=YAMLGetter): class HelpChannels(metaclass=YAMLGetter): section = 'help_channels' + enable: bool claim_minutes: int cmd_whitelist: List[int] idle_minutes: int diff --git a/config-default.yml b/config-default.yml index d87ceade7..12f69deca 100644 --- a/config-default.yml +++ b/config-default.yml @@ -505,6 +505,8 @@ mention: reset_delay: 5 help_channels: + enable: false + # Minimum interval before allowing a certain user to claim a new help channel claim_minutes: 15 -- cgit v1.2.3 From 28e9c74a57dbfac8049c90e7d128a041cbadedde Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 29 Mar 2020 17:00:45 -0700 Subject: HelpChannels: fix alphabetical sorting of dormant channels When a channel is moved, all channels below have their positions incremented by 1. This threw off the previous implementation which relied on position numbers being static. Co-authored-by: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> --- bot/cogs/help_channels.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 69af085ee..10b17cdb8 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,4 +1,5 @@ import asyncio +import bisect import inspect import itertools import json @@ -218,22 +219,30 @@ class HelpChannels(Scheduler, commands.Cog): return channel - def get_alphabetical_position(self, channel: discord.TextChannel) -> t.Optional[int]: - """ - Return the position to move `channel` to so alphabetic order is maintained. - - If the channel does not have a valid name with a chemical element, return None. - """ + @staticmethod + def get_position(channel: discord.TextChannel, destination: discord.CategoryChannel) -> int: + """Return alphabetical position for `channel` if moved to `destination`.""" log.trace(f"Getting alphabetical position for #{channel} ({channel.id}).") - try: - position = self.name_positions[channel.name] - except KeyError: - log.warning(f"Channel #{channel} ({channel.id}) doesn't have a valid name.") - position = None + # If the destination category is empty, use the first position + if not destination.channels: + position = 1 + else: + # Make a sorted list of channel names for bisect. + channel_names = [c.name for c in destination.channels] + + # Get location which would maintain sorted order if channel was inserted into the list. + rank = bisect.bisect(channel_names, channel.name) + + if rank == len(destination.channels): + # Channel should be moved to the end of the category. + position = destination.channels[-1].position + 1 + else: + # Channel should be moved to the position of its alphabetical successor. + position = destination.channels[rank].position log.trace( - f"Position of #{channel} ({channel.id}) in Dormant will be {position} " + f"Position of #{channel} ({channel.id}) in {destination.name} will be {position} " f"(was {channel.position})." ) @@ -438,7 +447,7 @@ class HelpChannels(Scheduler, commands.Cog): category=self.dormant_category, sync_permissions=True, topic=DORMANT_TOPIC, - position=self.get_alphabetical_position(channel), + position=self.get_position(channel, self.dormant_category), ) log.trace(f"Position of #{channel} ({channel.id}) is actually {channel.position}.") -- cgit v1.2.3 From 0c6a5a1da30fb22f3e10400fe99bc90d77926e5d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 29 Mar 2020 17:02:20 -0700 Subject: HelpChannels: remove positions from element names There is no longer a reliance on static alphabetical position numbers. --- bot/cogs/help_channels.py | 12 +-- bot/resources/elements.json | 240 ++++++++++++++++++++++---------------------- 2 files changed, 125 insertions(+), 127 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 10b17cdb8..3014cffa8 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,7 +1,6 @@ import asyncio import bisect import inspect -import itertools import json import logging import random @@ -259,9 +258,9 @@ class HelpChannels(Scheduler, commands.Cog): yield channel @staticmethod - def get_names() -> t.Dict[str, int]: + def get_names() -> t.List[str]: """ - Return a truncated dict of prefixed element names and their alphabetical indices. + Return a truncated list of prefixed element names. The amount of names if configured with `HelpChannels.max_total_channels`. The prefix is configured with `HelpChannels.name_prefix`. @@ -274,11 +273,10 @@ class HelpChannels(Scheduler, commands.Cog): with Path("bot/resources/elements.json").open(encoding="utf-8") as elements_file: all_names = json.load(elements_file) - names = itertools.islice(all_names.items(), count) if prefix: - names = ((prefix + name, pos) for name, pos in names) - - return dict(names) + return [prefix + name for name in all_names[:count]] + else: + return all_names[:count] def get_used_names(self) -> t.Set[str]: """Return channels names which are already being used.""" diff --git a/bot/resources/elements.json b/bot/resources/elements.json index 6ea4964aa..2dc9b6fd6 100644 --- a/bot/resources/elements.json +++ b/bot/resources/elements.json @@ -1,120 +1,120 @@ -{ - "hydrogen": 44, - "helium": 42, - "lithium": 53, - "beryllium": 9, - "boron": 12, - "carbon": 18, - "nitrogen": 69, - "oxygen": 73, - "fluorine": 34, - "neon": 64, - "sodium": 97, - "magnesium": 56, - "aluminium": 1, - "silicon": 95, - "phosphorus": 75, - "sulfur": 99, - "chlorine": 20, - "argon": 4, - "potassium": 79, - "calcium": 16, - "scandium": 92, - "titanium": 109, - "vanadium": 112, - "chromium": 21, - "manganese": 57, - "iron": 48, - "cobalt": 22, - "nickel": 66, - "copper": 24, - "zinc": 116, - "gallium": 37, - "germanium": 38, - "arsenic": 5, - "selenium": 94, - "bromine": 13, - "krypton": 49, - "rubidium": 88, - "strontium": 98, - "yttrium": 115, - "zirconium": 117, - "niobium": 68, - "molybdenum": 61, - "technetium": 101, - "ruthenium": 89, - "rhodium": 86, - "palladium": 74, - "silver": 96, - "cadmium": 14, - "indium": 45, - "tin": 108, - "antimony": 3, - "tellurium": 102, - "iodine": 46, - "xenon": 113, - "caesium": 15, - "barium": 7, - "lanthanum": 50, - "cerium": 19, - "praseodymium": 80, - "neodymium": 63, - "promethium": 81, - "samarium": 91, - "europium": 31, - "gadolinium": 36, - "terbium": 104, - "dysprosium": 28, - "holmium": 43, - "erbium": 30, - "thulium": 107, - "ytterbium": 114, - "lutetium": 55, - "hafnium": 40, - "tantalum": 100, - "tungsten": 110, - "rhenium": 85, - "osmium": 72, - "iridium": 47, - "platinum": 76, - "gold": 39, - "mercury": 60, - "thallium": 105, - "lead": 52, - "bismuth": 10, - "polonium": 78, - "astatine": 6, - "radon": 84, - "francium": 35, - "radium": 83, - "actinium": 0, - "thorium": 106, - "protactinium": 82, - "uranium": 111, - "neptunium": 65, - "plutonium": 77, - "americium": 2, - "curium": 25, - "berkelium": 8, - "californium": 17, - "einsteinium": 29, - "fermium": 32, - "mendelevium": 59, - "nobelium": 70, - "lawrencium": 51, - "rutherfordium": 90, - "dubnium": 27, - "seaborgium": 93, - "bohrium": 11, - "hassium": 41, - "meitnerium": 58, - "darmstadtium": 26, - "roentgenium": 87, - "copernicium": 23, - "nihonium": 67, - "flerovium": 33, - "moscovium": 62, - "livermorium": 54, - "tennessine": 103, - "oganesson": 71 -} +[ + "hydrogen", + "helium", + "lithium", + "beryllium", + "boron", + "carbon", + "nitrogen", + "oxygen", + "fluorine", + "neon", + "sodium", + "magnesium", + "aluminium", + "silicon", + "phosphorus", + "sulfur", + "chlorine", + "argon", + "potassium", + "calcium", + "scandium", + "titanium", + "vanadium", + "chromium", + "manganese", + "iron", + "cobalt", + "nickel", + "copper", + "zinc", + "gallium", + "germanium", + "arsenic", + "selenium", + "bromine", + "krypton", + "rubidium", + "strontium", + "yttrium", + "zirconium", + "niobium", + "molybdenum", + "technetium", + "ruthenium", + "rhodium", + "palladium", + "silver", + "cadmium", + "indium", + "tin", + "antimony", + "tellurium", + "iodine", + "xenon", + "caesium", + "barium", + "lanthanum", + "cerium", + "praseodymium", + "neodymium", + "promethium", + "samarium", + "europium", + "gadolinium", + "terbium", + "dysprosium", + "holmium", + "erbium", + "thulium", + "ytterbium", + "lutetium", + "hafnium", + "tantalum", + "tungsten", + "rhenium", + "osmium", + "iridium", + "platinum", + "gold", + "mercury", + "thallium", + "lead", + "bismuth", + "polonium", + "astatine", + "radon", + "francium", + "radium", + "actinium", + "thorium", + "protactinium", + "uranium", + "neptunium", + "plutonium", + "americium", + "curium", + "berkelium", + "californium", + "einsteinium", + "fermium", + "mendelevium", + "nobelium", + "lawrencium", + "rutherfordium", + "dubnium", + "seaborgium", + "bohrium", + "hassium", + "meitnerium", + "darmstadtium", + "roentgenium", + "copernicium", + "nihonium", + "flerovium", + "moscovium", + "livermorium", + "tennessine", + "oganesson" +] -- cgit v1.2.3 From 1509d3b1c39fac825d5e5a09fd0caf780db7c91c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 29 Mar 2020 17:13:02 -0700 Subject: BotCog: fix AttributeError getting a category for a DMChannel --- bot/cogs/bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/bot.py b/bot/cogs/bot.py index 267892cc3..f0ca2b175 100644 --- a/bot/cogs/bot.py +++ b/bot/cogs/bot.py @@ -225,7 +225,7 @@ class BotCog(Cog, name="Bot"): properly formatted Python syntax highlighting codeblocks. """ is_help_channel = ( - msg.channel.category + getattr(msg.channel, "category", None) and msg.channel.category.id in (Categories.help_available, Categories.help_in_use) ) parse_codeblock = ( -- cgit v1.2.3 From 00c9f092a0c056b21e012fdadd807d0410a3ca09 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 29 Mar 2020 17:22:53 -0700 Subject: HelpChannels: fix typo in docstring --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 3014cffa8..273c5d98c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -262,7 +262,7 @@ class HelpChannels(Scheduler, commands.Cog): """ Return a truncated list of prefixed element names. - The amount of names if configured with `HelpChannels.max_total_channels`. + The amount of names is configured with `HelpChannels.max_total_channels`. The prefix is configured with `HelpChannels.name_prefix`. """ count = constants.HelpChannels.max_total_channels -- cgit v1.2.3 From c289111feee7be3a82772bffe0d253dde77b3f01 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 30 Mar 2020 09:01:31 -0700 Subject: HelpChannels: fix typos in docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Leon Sandøy --- bot/cogs/help_channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 273c5d98c..984a11f61 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -279,7 +279,7 @@ class HelpChannels(Scheduler, commands.Cog): return all_names[:count] def get_used_names(self) -> t.Set[str]: - """Return channels names which are already being used.""" + """Return channel names which are already being used.""" log.trace("Getting channel names which are already being used.") names = set() @@ -371,7 +371,7 @@ class HelpChannels(Scheduler, commands.Cog): # Prevent the command from being used until ready. # The ready event wasn't used because channels could change categories between the time # the command is invoked and the cog is ready (e.g. if move_idle_channel wasn't called yet). - # This may confused users. So would potentially long delays for the cog to become ready. + # This may confuse users. So would potentially long delays for the cog to become ready. self.dormant_command.enabled = True await self.init_available() -- cgit v1.2.3 From 58fad6541fbff07c32883dcd2b4d046b1ef9d9b0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 30 Mar 2020 09:05:30 -0700 Subject: HelpChannels: use constant names instead of default values in docstring --- bot/cogs/help_channels.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 984a11f61..ff8d31ded 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -78,20 +78,19 @@ class HelpChannels(Scheduler, commands.Cog): Available Category * Contains channels which are ready to be occupied by someone who needs help - * Will always contain 2 channels; refilled automatically from the pool of dormant channels + * Will always contain `constants.HelpChannels.max_available` channels; refilled automatically + from the pool of dormant channels * Prioritise using the channels which have been dormant for the longest amount of time * If there are no more dormant channels, the bot will automatically create a new one - * Configurable with `constants.HelpChannels.max_available` * If there are no dormant channels to move, helpers will be notified (see `notify()`) * When a channel becomes available, the dormant embed will be edited to show `AVAILABLE_MSG` In Use Category * Contains all channels which are occupied by someone needing help - * Channel moves to dormant category after 45 minutes of being idle - * Configurable with `constants.HelpChannels.idle_minutes` - * Helpers+ command can prematurely mark a channel as dormant - * Configurable with `constants.HelpChannels.cmd_whitelist` + * Channel moves to dormant category after `constants.HelpChannels.idle_minutes` of being idle + * Command can prematurely mark a channel as dormant + * Allowed roles for the command are configurable with `constants.HelpChannels.cmd_whitelist` * When a channel becomes dormant, an embed with `DORMANT_MSG` will be sent Dormant Category -- cgit v1.2.3 From d37a0a16e391ad14f2569a245ee205223f8f26dc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 30 Mar 2020 09:41:00 -0700 Subject: ModLog: ignore update channel events for help channels The edit causes two channel update events to dispatch simultaneously: one for the channel topic changing and one for the category changing. The ModLog cog currently doesn't support ignoring multiple events of the same type for the same channel. Therefore, the ignore was hard coded rather than using the typical ignore mechanism. This is intended to be a temporary solution; it should be removed once the ModLog is changed to support this situation. --- bot/cogs/moderation/modlog.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py index c63b4bab9..beef7a8ef 100644 --- a/bot/cogs/moderation/modlog.py +++ b/bot/cogs/moderation/modlog.py @@ -15,7 +15,7 @@ from discord.ext.commands import Cog, Context from discord.utils import escape_markdown from bot.bot import Bot -from bot.constants import Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs +from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs from bot.utils.time import humanize_delta log = logging.getLogger(__name__) @@ -188,6 +188,12 @@ class ModLog(Cog, name="ModLog"): self._ignored[Event.guild_channel_update].remove(before.id) return + # Two channel updates are sent for a single edit: 1 for topic and 1 for category change. + # TODO: remove once support is added for ignoring multiple occurrences for the same channel. + help_categories = (Categories.help_available, Categories.help_dormant, Categories.help_in_use) + if after.category and after.category.id in help_categories: + return + diff = DeepDiff(before, after) changes = [] done = [] -- cgit v1.2.3 From 740bf3e81aba605cb2b4690e5bb25dcba85cd174 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 30 Mar 2020 10:11:24 -0700 Subject: HelpChannels: set to enabled by default --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 12f69deca..89f470e19 100644 --- a/config-default.yml +++ b/config-default.yml @@ -505,7 +505,7 @@ mention: reset_delay: 5 help_channels: - enable: false + enable: true # Minimum interval before allowing a certain user to claim a new help channel claim_minutes: 15 -- cgit v1.2.3 From 648bd4c760e73be9dc2fbac733fb0547bbb4a477 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sun, 5 Apr 2020 13:57:21 +0100 Subject: Reduce span of hyperlink in AVAILABLE_MSG and DORMANT_MSG --- bot/cogs/help_channels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index ff8d31ded..2b0b463c4 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -47,7 +47,7 @@ currently cannot send a message in this channel, it means you are on cooldown an Try to write the best question you can by providing a detailed description and telling us what \ you've tried already. For more information on asking a good question, \ -[check out our guide on asking good questions]({ASKING_GUIDE_URL}). +check out our guide on [asking good questions]({ASKING_GUIDE_URL}). """ DORMANT_MSG = f""" @@ -58,7 +58,7 @@ channel until it becomes available again. If your question wasn't answered yet, you can claim a new help channel from the \ **Help: Available** category by simply asking your question again. Consider rephrasing the \ question to maximize your chance of getting a good answer. If you're not sure how, have a look \ -through [our guide for asking a good question]({ASKING_GUIDE_URL}). +through our guide for [asking a good question]({ASKING_GUIDE_URL}). """ -- cgit v1.2.3 From 0da93b1b64368cbcf3fe14dd1fe923a3fd0af427 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sun, 5 Apr 2020 14:00:31 +0100 Subject: Add close alias for dormant command --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 2b0b463c4..b820c7ad3 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -184,7 +184,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Populating the name queue with names.") return deque(available_names) - @commands.command(name="dormant", enabled=False) + @commands.command(name="dormant", aliases=["close"], enabled=False) @with_role(*constants.HelpChannels.cmd_whitelist) async def dormant_command(self, ctx: commands.Context) -> None: """Make the current in-use help channel dormant.""" -- cgit v1.2.3 From 7571cabe65e39d231523e713923cd23b927225bc Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Sun, 5 Apr 2020 17:09:35 +0200 Subject: Change help channel sorting to bottom position The current sorting algorithm we used created unpredictable channel order (for our human end-users) and induced a flickering channel light-show in Discord clients. To combat these undesirable side-effects, I've changed the ordering to always order channels at the bottom of a category. This also means that channels looking for answers the longest will naturally float up. --- bot/cogs/help_channels.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index b820c7ad3..68dbdf9ed 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -1,5 +1,4 @@ import asyncio -import bisect import inspect import json import logging @@ -219,25 +218,15 @@ class HelpChannels(Scheduler, commands.Cog): @staticmethod def get_position(channel: discord.TextChannel, destination: discord.CategoryChannel) -> int: - """Return alphabetical position for `channel` if moved to `destination`.""" - log.trace(f"Getting alphabetical position for #{channel} ({channel.id}).") + """Return the position to sort the `channel` at the bottom if moved to `destination`.""" + log.trace(f"Getting bottom position for #{channel} ({channel.id}).") - # If the destination category is empty, use the first position if not destination.channels: + # If the destination category is empty, use the first position position = 1 else: - # Make a sorted list of channel names for bisect. - channel_names = [c.name for c in destination.channels] - - # Get location which would maintain sorted order if channel was inserted into the list. - rank = bisect.bisect(channel_names, channel.name) - - if rank == len(destination.channels): - # Channel should be moved to the end of the category. - position = destination.channels[-1].position + 1 - else: - # Channel should be moved to the position of its alphabetical successor. - position = destination.channels[rank].position + # Else use the maximum position int + 1 + position = max(c.position for c in destination.channels) + 1 log.trace( f"Position of #{channel} ({channel.id}) in {destination.name} will be {position} " @@ -464,7 +453,7 @@ class HelpChannels(Scheduler, commands.Cog): category=self.in_use_category, sync_permissions=True, topic=IN_USE_TOPIC, - position=0, + position=self.get_position(channel, self.in_use_category), ) timeout = constants.HelpChannels.idle_minutes * 60 -- cgit v1.2.3 From 4cb5030fd524c4dd8adce9c9e1fe9cc26228ad9b Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 6 Apr 2020 17:25:44 +0200 Subject: Add channel status emoji to help channels I've added channel status emojis as a prefix to our help channels to make it more obvious to the end user what the current status of a channel is. All channels in the Available category will be marked with a green checkmark emoji, while all channels in the In Use category will be marked with an hourglass. Channels in the Dormant category stay unadorned. Channels will be stripped of their previous prefix when moved to another category. This relies on the `help-` naming convention, as that is the most reliable way to do it that does not break if we ever opt for another emoji. --- bot/cogs/help_channels.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 68dbdf9ed..4fddba627 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -60,6 +60,10 @@ question to maximize your chance of getting a good answer. If you're not sure ho through our guide for [asking a good question]({ASKING_GUIDE_URL}). """ +AVAILABLE_EMOJI = "✅" +IN_USE_EMOJI = "⌛" +NAME_SEPARATOR = "|" + class TaskData(t.NamedTuple): """Data for a scheduled task.""" @@ -235,6 +239,20 @@ class HelpChannels(Scheduler, commands.Cog): return position + @staticmethod + def get_clean_channel_name(channel: discord.TextChannel) -> str: + """Return a clean channel name without status emojis prefix.""" + try: + # Try to remove the status prefix using the index of "help-" + name = channel.name[channel.name.index("help-"):] + log.trace(f"The clean name for `{channel}` is `{name}`") + except ValueError: + # If, for some reason, the channel name does not contain "help-" fall back gracefully + log.info(f"Can't get clean name as `{channel}` does not follow the `help-` naming convention.") + name = channel.name + + return name + @staticmethod def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: """Yield the text channels of the `category` in an unsorted manner.""" @@ -419,7 +437,9 @@ class HelpChannels(Scheduler, commands.Cog): await self.send_available_message(channel) log.trace(f"Moving #{channel} ({channel.id}) to the Available category.") + await channel.edit( + name=f"{AVAILABLE_EMOJI}{NAME_SEPARATOR}{self.get_clean_channel_name(channel)}", category=self.available_category, sync_permissions=True, topic=AVAILABLE_TOPIC, @@ -430,6 +450,7 @@ class HelpChannels(Scheduler, commands.Cog): log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") await channel.edit( + name=self.get_clean_channel_name(channel), category=self.dormant_category, sync_permissions=True, topic=DORMANT_TOPIC, @@ -450,6 +471,7 @@ class HelpChannels(Scheduler, commands.Cog): log.info(f"Moving #{channel} ({channel.id}) to the In Use category.") await channel.edit( + name=f"{IN_USE_EMOJI}{NAME_SEPARATOR}{self.get_clean_channel_name(channel)}", category=self.in_use_category, sync_permissions=True, topic=IN_USE_TOPIC, -- cgit v1.2.3 From ae49def47a2f956b93c814993b8f380b5182c6b7 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 6 Apr 2020 17:32:57 +0200 Subject: Change bottom sorting strategy to using a large int The current approach of trying to find the maximum channel position, adding one, and using that as the position integer for channels does not seem to work reliably. An approach that seems to work in the testing environment is using a very large integer for the position attribute of the channel: It wil be sorted at the bottom and Discord will automatically scale the integer down to `max + 1`. This also means the `get_position` utility function is no longer needed; it has been removed. --- bot/cogs/help_channels.py | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 4fddba627..60580695c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -220,25 +220,6 @@ class HelpChannels(Scheduler, commands.Cog): return channel - @staticmethod - def get_position(channel: discord.TextChannel, destination: discord.CategoryChannel) -> int: - """Return the position to sort the `channel` at the bottom if moved to `destination`.""" - log.trace(f"Getting bottom position for #{channel} ({channel.id}).") - - if not destination.channels: - # If the destination category is empty, use the first position - position = 1 - else: - # Else use the maximum position int + 1 - position = max(c.position for c in destination.channels) + 1 - - log.trace( - f"Position of #{channel} ({channel.id}) in {destination.name} will be {position} " - f"(was {channel.position})." - ) - - return position - @staticmethod def get_clean_channel_name(channel: discord.TextChannel) -> str: """Return a clean channel name without status emojis prefix.""" @@ -454,7 +435,7 @@ class HelpChannels(Scheduler, commands.Cog): category=self.dormant_category, sync_permissions=True, topic=DORMANT_TOPIC, - position=self.get_position(channel, self.dormant_category), + position=10000, ) log.trace(f"Position of #{channel} ({channel.id}) is actually {channel.position}.") @@ -475,7 +456,7 @@ class HelpChannels(Scheduler, commands.Cog): category=self.in_use_category, sync_permissions=True, topic=IN_USE_TOPIC, - position=self.get_position(channel, self.in_use_category), + position=10000, ) timeout = constants.HelpChannels.idle_minutes * 60 -- cgit v1.2.3 From dfbecd2077c72d7475aaece4a3f92a12b56a207c Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 6 Apr 2020 18:06:39 +0200 Subject: Use configurable prefix to clean help channel names The help channel prefix is configurable as a constant, but I accidentally used a static prefix in the utility function that cleaned the channel names. This commit makes sure the utility method uses the prefix defined in the constants. --- bot/cogs/help_channels.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 60580695c..2e203df46 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -223,13 +223,14 @@ class HelpChannels(Scheduler, commands.Cog): @staticmethod def get_clean_channel_name(channel: discord.TextChannel) -> str: """Return a clean channel name without status emojis prefix.""" + prefix = constants.HelpChannels.name_prefix try: - # Try to remove the status prefix using the index of "help-" - name = channel.name[channel.name.index("help-"):] + # Try to remove the status prefix using the index of the channel prefix + name = channel.name[channel.name.index(prefix):] log.trace(f"The clean name for `{channel}` is `{name}`") except ValueError: # If, for some reason, the channel name does not contain "help-" fall back gracefully - log.info(f"Can't get clean name as `{channel}` does not follow the `help-` naming convention.") + log.info(f"Can't get clean name as `{channel}` does not follow the `{prefix}` naming convention.") name = channel.name return name -- cgit v1.2.3 From e3d7afa44346ee7d2e123668e55b623dc901515d Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Mon, 6 Apr 2020 21:18:09 +0200 Subject: Use clean help channel name for used name set The set that keeps track of the used channel names should discard emojis. To do that, I'm cleaning the names before they're added to the set of channel names. --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 2e203df46..1e062ca46 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -273,7 +273,7 @@ class HelpChannels(Scheduler, commands.Cog): names = set() for cat in (self.available_category, self.in_use_category, self.dormant_category): for channel in self.get_category_channels(cat): - names.add(channel.name) + names.add(self.get_clean_channel_name(channel)) if len(names) > MAX_CHANNELS_PER_CATEGORY: log.warning( -- cgit v1.2.3 From c4015d8137e7541f1e76666e28b3e707524e7d06 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 7 Apr 2020 09:30:37 +0200 Subject: Set the ID of the new Help: In Use category As Discord is having a rather persistent issue with one of the channels in the current `Help: In Use` category, we're going to start using a new category that excludes the old channel. The old channel, help-argon, appears to be completely broken on Discord's end, resulting in "Not found" errors for any kind of interaction, including channel move and/or channel delete admin actions. As it's still visible, it's currently triggering a lot questions from our members. We hope that using a new category will fix that. --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 70c31ebb5..896003973 100644 --- a/config-default.yml +++ b/config-default.yml @@ -112,7 +112,7 @@ guild: categories: help_available: 691405807388196926 - help_in_use: 356013061213126657 + help_in_use: 696958401460043776 help_dormant: 691405908919451718 channels: -- cgit v1.2.3 From 0460dc29a5be15d9e11fd70d37041affe1cd4ba2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 7 Apr 2020 20:04:24 +0200 Subject: Change help available embed to use occupied term The embed displayed in available help channels still used the term "in use" instead of "occupied". I've updated the embed to reflect the new name of the "occupied" category. --- bot/cogs/help_channels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 1e062ca46..915961b34 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -37,9 +37,9 @@ channels in the Help: Available category. AVAILABLE_MSG = f""" This help channel is now **available**, which means that you can claim it by simply typing your \ -question into it. Once claimed, the channel will move into the **Help: In Use** category, and will \ -be yours until it has been inactive for {constants.HelpChannels.idle_minutes} minutes. When that \ -happens, it will be set to **dormant** and moved into the **Help: Dormant** category. +question into it. Once claimed, the channel will move into the **Python Help: Occupied** category, \ +and will be yours until it has been inactive for {constants.HelpChannels.idle_minutes} minutes. When \ +that happens, it will be set to **dormant** and moved into the **Help: Dormant** category. You may claim a new channel once every {constants.HelpChannels.claim_minutes} minutes. If you \ currently cannot send a message in this channel, it means you are on cooldown and need to wait. -- cgit v1.2.3 From 8ca0b7f5161f0a71df39a36758b3d8041c895fe8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Tue, 7 Apr 2020 20:06:00 +0200 Subject: Ensure available help channels sync their permissions The help channels in the `Help: Available` category should automatically synchronize their permissions with the category permissions to ensure that the overwrites we use to prevent people from claiming multiple help channels are properly enforced. Unfortunately, for unknown reasons, they sometimes get in an "out of sync" state that requires intervention to get them back in sync. This PR mitigates that issue by checking the available channel for their synchronisation status during certain critical times in our help channel system: 1. Whenever the overwrites for the category change 2. Whenever a channel is moved into the new category 3. After the categories have been reset during the initialization process The check is straightforward: The `ensure_permissions_synchronization` method iterates over all the channels in the category and checks if the channels are currently synchronizing their permissions. If not, we remedy that by making a channel edit request to the Discord API. If all channels were already "in sync", no API calls are made. The latter should make this an inexpensive mitigation procedure: As we typically have very few channels in the available category and channels mostly stay in sync, we typically do very little. To make this process a bit easier, I've factored out `set_permissions` calls to a helper function that also calls the `ensure_permissions_synchronization` method. The only exception is during the reset process: As we may edit multiple permissions in this loop, it's better to only ensure the synchronization after we're done with all permission changes. --- bot/cogs/help_channels.py | 51 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 915961b34..697a4d3b7 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -427,6 +427,12 @@ class HelpChannels(Scheduler, commands.Cog): topic=AVAILABLE_TOPIC, ) + log.trace( + f"Ensuring that all channels in `{self.available_category}` have " + f"synchronized permissions after moving `{channel}` into it." + ) + await self.ensure_permissions_synchronization(self.available_category) + async def move_to_dormant(self, channel: discord.TextChannel) -> None: """Make the `channel` dormant.""" log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") @@ -544,6 +550,39 @@ class HelpChannels(Scheduler, commands.Cog): # be put in the queue. await self.move_to_available() + @staticmethod + async def ensure_permissions_synchronization(category: discord.CategoryChannel) -> None: + """ + Ensure that all channels in the `category` have their permissions synchronized. + + This method mitigates an issue we have yet to find the cause for: Every so often, a channel in the + `Help: Available` category gets in a state in which it will no longer synchronizes its permissions + with the category. To prevent that, we iterate over the channels in the category and edit the channels + that are observed to be in such a state. If no "out of sync" channels are observed, this method will + not make API calls and should be fairly inexpensive to run. + """ + for channel in category.channels: + if not channel.permissions_synced: + log.info(f"The permissions of channel `{channel}` were out of sync with category `{category}`.") + await channel.edit(sync_permissions=True) + + async def update_category_permissions( + self, category: discord.CategoryChannel, member: discord.Member, **permissions + ) -> None: + """ + Update the permissions of the given `member` for the given `category` with `permissions` passed. + + After updating the permissions for the member in the category, this helper function will call the + `ensure_permissions_synchronization` method to ensure that all channels are still synchronizing their + permissions with the category. It's currently unknown why some channels get "out of sync", but this + hopefully mitigates the issue. + """ + log.trace(f"Updating permissions for `{member}` in `{category}` with {permissions}.") + await category.set_permissions(member, **permissions) + + log.trace(f"Ensuring that all channels in `{category}` are synchronized after permissions update.") + await self.ensure_permissions_synchronization(category) + async def reset_send_permissions(self) -> None: """Reset send permissions for members with it set to False in the Available category.""" log.trace("Resetting send permissions in the Available category.") @@ -551,7 +590,13 @@ class HelpChannels(Scheduler, commands.Cog): for member, overwrite in self.available_category.overwrites.items(): if isinstance(member, discord.Member) and overwrite.send_messages is False: log.trace(f"Resetting send permissions for {member} ({member.id}).") - await self.available_category.set_permissions(member, send_messages=None) + + # We don't use the permissions helper function here as we may have to reset multiple overwrites + # and we don't want to enforce the permissions synchronization in each iteration. + await self.available_category.set_permissions(member, overwrite=None) + + log.trace(f"Ensuring channels in `Help: Available` are synchronized after permissions reset.") + await self.ensure_permissions_synchronization(self.available_category) async def revoke_send_permissions(self, member: discord.Member) -> None: """ @@ -564,14 +609,14 @@ class HelpChannels(Scheduler, commands.Cog): f"Revoking {member}'s ({member.id}) send message permissions in the Available category." ) - await self.available_category.set_permissions(member, send_messages=False) + await self.update_category_permissions(self.available_category, member, send_messages=False) # Cancel the existing task, if any. # Would mean the user somehow bypassed the lack of permissions (e.g. user is guild owner). self.cancel_task(member.id, ignore_missing=True) timeout = constants.HelpChannels.claim_minutes * 60 - callback = self.available_category.set_permissions(member, overwrite=None) + callback = self.update_category_permissions(self.available_category, member, overwrite=None) log.trace(f"Scheduling {member}'s ({member.id}) send message permissions to be reinstated.") self.schedule_task(member.id, TaskData(timeout, callback)) -- cgit v1.2.3 From 12c80fb664e612a2319dfde7d341737159934e9c Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff <33516116+SebastiaanZ@users.noreply.github.com> Date: Wed, 8 Apr 2020 19:08:50 +0200 Subject: Stop setting positions when moving help channels Unfortunately, trying to set positions for the help channels during their move from one category to another does not work to well. It introduces a number of glitches and we haven't been able to reliably get a channel to go to a specific position either. This commit simply removes any attempt to set a position and lets Discord handle it. --- bot/cogs/help_channels.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 697a4d3b7..797019f69 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -442,7 +442,6 @@ class HelpChannels(Scheduler, commands.Cog): category=self.dormant_category, sync_permissions=True, topic=DORMANT_TOPIC, - position=10000, ) log.trace(f"Position of #{channel} ({channel.id}) is actually {channel.position}.") @@ -463,7 +462,6 @@ class HelpChannels(Scheduler, commands.Cog): category=self.in_use_category, sync_permissions=True, topic=IN_USE_TOPIC, - position=10000, ) timeout = constants.HelpChannels.idle_minutes * 60 -- cgit v1.2.3 From c608963a0672b8396190f521366799d649a8cb90 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Apr 2020 00:15:07 +0200 Subject: Remove dormant invokation message after move. --- bot/cogs/help_channels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 697a4d3b7..75b61f6cb 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -196,6 +196,7 @@ class HelpChannels(Scheduler, commands.Cog): if ctx.channel.category == self.in_use_category: self.cancel_task(ctx.channel.id) await self.move_to_dormant(ctx.channel) + await ctx.message.delete() else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") -- cgit v1.2.3 From 1a58b34565e612a48d8dc59cb3f4ed75ee593744 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Apr 2020 01:46:57 +0200 Subject: Allow help session starters to invoke dormant. Removing the `with_role` check from the command and replcaing it with a new `dormant_check` that's used in the body, which also checks against a cache of users that started the sessions, allows them to close their own channels along with the role check. --- bot/cogs/help_channels.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 75b61f6cb..cdfe4e72c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -13,7 +13,7 @@ from discord.ext import commands from bot import constants from bot.bot import Bot -from bot.decorators import with_role +from bot.utils.checks import with_role_check from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) @@ -108,6 +108,7 @@ class HelpChannels(Scheduler, commands.Cog): super().__init__() self.bot = bot + self.help_channel_users: t.Dict[discord.User, discord.TextChannel] = {} # Categories self.available_category: discord.CategoryChannel = None @@ -187,16 +188,24 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Populating the name queue with names.") return deque(available_names) + async def dormant_check(self, ctx: commands.Context) -> bool: + """Return True if the user started the help channel session or passes the role check.""" + if self.help_channel_users.get(ctx.author) == ctx.channel: + log.trace(f"{ctx.author} started the help session, passing the check for dormant.") + return True + + log.trace(f"{ctx.author} did not start the help session, checking roles.") + return with_role_check(ctx, *constants.HelpChannels.cmd_whitelist) + @commands.command(name="dormant", aliases=["close"], enabled=False) - @with_role(*constants.HelpChannels.cmd_whitelist) async def dormant_command(self, ctx: commands.Context) -> None: """Make the current in-use help channel dormant.""" log.trace("dormant command invoked; checking if the channel is in-use.") - if ctx.channel.category == self.in_use_category: - self.cancel_task(ctx.channel.id) - await self.move_to_dormant(ctx.channel) - await ctx.message.delete() + if await self.dormant_check(ctx): + self.cancel_task(ctx.channel.id) + await self.move_to_dormant(ctx.channel) + await ctx.message.delete() else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") @@ -543,6 +552,8 @@ class HelpChannels(Scheduler, commands.Cog): await self.move_to_in_use(channel) await self.revoke_send_permissions(message.author) + # Add user with channel for dormant check. + self.help_channel_users[message.author] = channel log.trace(f"Releasing on_message lock for {message.id}.") -- cgit v1.2.3 From 7ccfa21f38b0df50f582ceb6e64d1ce1fe9c6617 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Apr 2020 02:02:41 +0200 Subject: Reset cooldown after channel is made dormant. --- bot/cogs/help_channels.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index cdfe4e72c..7bfb33875 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -206,6 +206,7 @@ class HelpChannels(Scheduler, commands.Cog): self.cancel_task(ctx.channel.id) await self.move_to_dormant(ctx.channel) await ctx.message.delete() + await self.reset_send_permissions_for_help_user(ctx.channel) else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") @@ -610,6 +611,19 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Ensuring channels in `Help: Available` are synchronized after permissions reset.") await self.ensure_permissions_synchronization(self.available_category) + async def reset_send_permissions_for_help_user(self, channel: discord.TextChannel) -> None: + """Reset send permissions in the Available category for member that started the help session in `channel`.""" + # Get mapping of channels to users that started the help session in them. + channels_to_users = {value: key for key, value in self.help_channel_users.items()} + log.trace(f"Attempting to find user for help session in #{channel.name} ({channel.id}).") + try: + member: discord.Member = channels_to_users[channel] + except KeyError: + log.trace(f"Channel #{channel.name} ({channel.id}) not in help session cache, permissions unchanged.") + return + log.trace(f"Resetting send permissions for {member} ({member.id}).") + await self.available_category.set_permissions(member, send_messages=None) + async def revoke_send_permissions(self, member: discord.Member) -> None: """ Disallow `member` to send messages in the Available category for a certain time. -- cgit v1.2.3 From c6355f67192e247f6b207cb0e18b2ff80d5b710f Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Apr 2020 02:03:13 +0200 Subject: Extend docstrings to include new behaviour. --- bot/cogs/help_channels.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 7bfb33875..912ce4f44 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -199,7 +199,13 @@ class HelpChannels(Scheduler, commands.Cog): @commands.command(name="dormant", aliases=["close"], enabled=False) async def dormant_command(self, ctx: commands.Context) -> None: - """Make the current in-use help channel dormant.""" + """ + Make the current in-use help channel dormant. + + Make the channel dormant if the user passes the `dormant_check`, + delete the message that invoked this, + and reset the send permissions cooldown for the user who started the session. + """ log.trace("dormant command invoked; checking if the channel is in-use.") if ctx.channel.category == self.in_use_category: if await self.dormant_check(ctx): -- cgit v1.2.3 From acee84d044c65b9a8d6ab4a164d189fa0eaa174a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Apr 2020 02:06:07 +0200 Subject: Handle dormant invokation not being found. --- bot/cogs/help_channels.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 912ce4f44..bc973cd4d 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -5,6 +5,7 @@ import logging import random import typing as t from collections import deque +from contextlib import suppress from datetime import datetime from pathlib import Path @@ -211,7 +212,9 @@ class HelpChannels(Scheduler, commands.Cog): if await self.dormant_check(ctx): self.cancel_task(ctx.channel.id) await self.move_to_dormant(ctx.channel) - await ctx.message.delete() + with suppress(discord.errors.NotFound): + await ctx.message.delete() + log.trace("Deleting dormant invokation message.") await self.reset_send_permissions_for_help_user(ctx.channel) else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") -- cgit v1.2.3 From c2e9ec459cd28832a1e797d7c755b5aab57d69dd Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Apr 2020 02:13:36 +0200 Subject: Cancel permission restoration task. After the dormant command is used and the permissions are restored for the user that started the session, the task for restoring them after the claim time has passed is no longer necessary. --- bot/cogs/help_channels.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index bc973cd4d..03bac27a4 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -632,6 +632,8 @@ class HelpChannels(Scheduler, commands.Cog): return log.trace(f"Resetting send permissions for {member} ({member.id}).") await self.available_category.set_permissions(member, send_messages=None) + # Cancel task, ignore no task existing when the claim time passed but idle time has not. + self.cancel_task(member.id, ignore_missing=True) async def revoke_send_permissions(self, member: discord.Member) -> None: """ -- cgit v1.2.3 From 7f1be2882dde670d12000fe661424d3b6f406f89 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Apr 2020 22:34:17 +0200 Subject: Reverse help_channel_user pairs. Pairing users to channels was a design flaw, because the keys didn't get overwritten. This allowed multiple users to access the dormant command for the running session of the bot. Replacing this with a reversed paring fixes both issues because the cache is overwritten on channel activation. --- bot/cogs/help_channels.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 03bac27a4..dbe7cedc1 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -109,7 +109,7 @@ class HelpChannels(Scheduler, commands.Cog): super().__init__() self.bot = bot - self.help_channel_users: t.Dict[discord.User, discord.TextChannel] = {} + self.help_channel_users: t.Dict[discord.TextChannel, discord.User] = {} # Categories self.available_category: discord.CategoryChannel = None @@ -191,7 +191,7 @@ class HelpChannels(Scheduler, commands.Cog): async def dormant_check(self, ctx: commands.Context) -> bool: """Return True if the user started the help channel session or passes the role check.""" - if self.help_channel_users.get(ctx.author) == ctx.channel: + if self.help_channel_users.get(ctx.channel) == ctx.author: log.trace(f"{ctx.author} started the help session, passing the check for dormant.") return True @@ -563,7 +563,7 @@ class HelpChannels(Scheduler, commands.Cog): await self.move_to_in_use(channel) await self.revoke_send_permissions(message.author) # Add user with channel for dormant check. - self.help_channel_users[message.author] = channel + self.help_channel_users[channel] = message.author log.trace(f"Releasing on_message lock for {message.id}.") @@ -622,11 +622,9 @@ class HelpChannels(Scheduler, commands.Cog): async def reset_send_permissions_for_help_user(self, channel: discord.TextChannel) -> None: """Reset send permissions in the Available category for member that started the help session in `channel`.""" - # Get mapping of channels to users that started the help session in them. - channels_to_users = {value: key for key, value in self.help_channel_users.items()} log.trace(f"Attempting to find user for help session in #{channel.name} ({channel.id}).") try: - member: discord.Member = channels_to_users[channel] + member = self.help_channel_users[channel] except KeyError: log.trace(f"Channel #{channel.name} ({channel.id}) not in help session cache, permissions unchanged.") return -- cgit v1.2.3 From 25c15171020613f0123ed83654adad5c7c584d84 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Apr 2020 01:11:37 +0200 Subject: Delete overwrite instead of send_messages permission. Only resetting the permission caused the overwrites for the users to remain on the category, potentially piling up and causing further issues. --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index dbe7cedc1..f49149d8a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -629,7 +629,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Channel #{channel.name} ({channel.id}) not in help session cache, permissions unchanged.") return log.trace(f"Resetting send permissions for {member} ({member.id}).") - await self.available_category.set_permissions(member, send_messages=None) + await self.available_category.set_permissions(member, overwrite=None) # Cancel task, ignore no task existing when the claim time passed but idle time has not. self.cancel_task(member.id, ignore_missing=True) -- cgit v1.2.3 From 4d6c342d2a35a54f65f25a973994c7dc55ca4be8 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Apr 2020 01:12:57 +0200 Subject: Change names to more descriptive ones. --- bot/cogs/help_channels.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index f49149d8a..73c7adb15 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -109,7 +109,7 @@ class HelpChannels(Scheduler, commands.Cog): super().__init__() self.bot = bot - self.help_channel_users: t.Dict[discord.TextChannel, discord.User] = {} + self.help_channel_claimants: t.Dict[discord.TextChannel, discord.User] = {} # Categories self.available_category: discord.CategoryChannel = None @@ -191,7 +191,7 @@ class HelpChannels(Scheduler, commands.Cog): async def dormant_check(self, ctx: commands.Context) -> bool: """Return True if the user started the help channel session or passes the role check.""" - if self.help_channel_users.get(ctx.channel) == ctx.author: + if self.help_channel_claimants.get(ctx.channel) == ctx.author: log.trace(f"{ctx.author} started the help session, passing the check for dormant.") return True @@ -215,7 +215,7 @@ class HelpChannels(Scheduler, commands.Cog): with suppress(discord.errors.NotFound): await ctx.message.delete() log.trace("Deleting dormant invokation message.") - await self.reset_send_permissions_for_help_user(ctx.channel) + await self.reset_claimant_send_permission(ctx.channel) else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") @@ -563,7 +563,7 @@ class HelpChannels(Scheduler, commands.Cog): await self.move_to_in_use(channel) await self.revoke_send_permissions(message.author) # Add user with channel for dormant check. - self.help_channel_users[channel] = message.author + self.help_channel_claimants[channel] = message.author log.trace(f"Releasing on_message lock for {message.id}.") @@ -620,11 +620,11 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Ensuring channels in `Help: Available` are synchronized after permissions reset.") await self.ensure_permissions_synchronization(self.available_category) - async def reset_send_permissions_for_help_user(self, channel: discord.TextChannel) -> None: + async def reset_claimant_send_permission(self, channel: discord.TextChannel) -> None: """Reset send permissions in the Available category for member that started the help session in `channel`.""" log.trace(f"Attempting to find user for help session in #{channel.name} ({channel.id}).") try: - member = self.help_channel_users[channel] + member = self.help_channel_claimants[channel] except KeyError: log.trace(f"Channel #{channel.name} ({channel.id}) not in help session cache, permissions unchanged.") return -- cgit v1.2.3 From 52f19185752358cff21aadc7902493f78030a94f Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Apr 2020 01:14:21 +0200 Subject: Reword strings to reflect name changes. --- bot/cogs/help_channels.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 73c7adb15..3418ad210 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -190,12 +190,12 @@ class HelpChannels(Scheduler, commands.Cog): return deque(available_names) async def dormant_check(self, ctx: commands.Context) -> bool: - """Return True if the user started the help channel session or passes the role check.""" + """Return True if the user is the help channel claimant or passes the role check.""" if self.help_channel_claimants.get(ctx.channel) == ctx.author: - log.trace(f"{ctx.author} started the help session, passing the check for dormant.") + log.trace(f"{ctx.author} is the help channel claimant, passing the check for dormant.") return True - log.trace(f"{ctx.author} did not start the help session, checking roles.") + log.trace(f"{ctx.author} is not the help channel claimant, checking roles.") return with_role_check(ctx, *constants.HelpChannels.cmd_whitelist) @commands.command(name="dormant", aliases=["close"], enabled=False) @@ -621,12 +621,12 @@ class HelpChannels(Scheduler, commands.Cog): await self.ensure_permissions_synchronization(self.available_category) async def reset_claimant_send_permission(self, channel: discord.TextChannel) -> None: - """Reset send permissions in the Available category for member that started the help session in `channel`.""" - log.trace(f"Attempting to find user for help session in #{channel.name} ({channel.id}).") + """Reset send permissions in the Available category for the help `channel` claimant.""" + log.trace(f"Attempting to find claimant for #{channel.name} ({channel.id}).") try: member = self.help_channel_claimants[channel] except KeyError: - log.trace(f"Channel #{channel.name} ({channel.id}) not in help session cache, permissions unchanged.") + log.trace(f"Channel #{channel.name} ({channel.id}) not in claimant cache, permissions unchanged.") return log.trace(f"Resetting send permissions for {member} ({member.id}).") await self.available_category.set_permissions(member, overwrite=None) -- cgit v1.2.3 From a0daa7e77c4920fc51229fb751a48d08280ed42d Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Apr 2020 01:20:03 +0200 Subject: Add spacing. Co-authored-by: MarkKoz --- bot/cogs/help_channels.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 3418ad210..421d41baf 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -628,6 +628,7 @@ class HelpChannels(Scheduler, commands.Cog): except KeyError: log.trace(f"Channel #{channel.name} ({channel.id}) not in claimant cache, permissions unchanged.") return + log.trace(f"Resetting send permissions for {member} ({member.id}).") await self.available_category.set_permissions(member, overwrite=None) # Cancel task, ignore no task existing when the claim time passed but idle time has not. -- cgit v1.2.3 From 3532d06db7083c8a7dc6a4b87a28d11423d2e605 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Tue, 7 Apr 2020 01:20:48 +0200 Subject: Reword comment. Co-authored-by: MarkKoz --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 421d41baf..52af03d27 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -631,7 +631,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Resetting send permissions for {member} ({member.id}).") await self.available_category.set_permissions(member, overwrite=None) - # Cancel task, ignore no task existing when the claim time passed but idle time has not. + # Ignore missing task when claim cooldown has passed but the channel still isn't dormant. self.cancel_task(member.id, ignore_missing=True) async def revoke_send_permissions(self, member: discord.Member) -> None: -- cgit v1.2.3 From 44333ae53b6692ab34b4fe967ecbf4f215e7fb0e Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 9 Apr 2020 15:20:02 +0200 Subject: Move message deletion up. --- bot/cogs/help_channels.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 52af03d27..8dca2ede0 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -210,11 +210,12 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("dormant command invoked; checking if the channel is in-use.") if ctx.channel.category == self.in_use_category: if await self.dormant_check(ctx): - self.cancel_task(ctx.channel.id) - await self.move_to_dormant(ctx.channel) with suppress(discord.errors.NotFound): - await ctx.message.delete() log.trace("Deleting dormant invokation message.") + await ctx.message.delete() + + self.cancel_task(ctx.channel.id) + await self.move_to_dormant(ctx.channel) await self.reset_claimant_send_permission(ctx.channel) else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") -- cgit v1.2.3 From 93a511da2be49305bc27fcce18f10f9758418dc5 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 9 Apr 2020 15:22:55 +0200 Subject: Move permissions reset up. --- bot/cogs/help_channels.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 8dca2ede0..f53f7a7ba 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -214,9 +214,10 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Deleting dormant invokation message.") await ctx.message.delete() + await self.reset_claimant_send_permission(ctx.channel) + self.cancel_task(ctx.channel.id) await self.move_to_dormant(ctx.channel) - await self.reset_claimant_send_permission(ctx.channel) else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") -- cgit v1.2.3 From 221426d91fa1db5f334562ac0af52d93bbe7ab10 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 9 Apr 2020 15:23:26 +0200 Subject: Suppress errors when resetting permissions. --- bot/cogs/help_channels.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index f53f7a7ba..a6fa05d90 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -214,7 +214,8 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("Deleting dormant invokation message.") await ctx.message.delete() - await self.reset_claimant_send_permission(ctx.channel) + with suppress(discord.errors.HTTPException, discord.errors.NotFound): + await self.reset_claimant_send_permission(ctx.channel) self.cancel_task(ctx.channel.id) await self.move_to_dormant(ctx.channel) -- cgit v1.2.3 From 9d6425474b8efa2dd4aba0086c1e8aaeb5eafeed Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 9 Apr 2020 08:00:39 -0700 Subject: HelpChannels: check author of dormant message In a testing environment, the bot may try to edit the message of a different bot. Therefore, the author of the message should be checked to ensure the current bot sent it. --- bot/cogs/help_channels.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index a6fa05d90..5f59a9d60 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -390,14 +390,13 @@ class HelpChannels(Scheduler, commands.Cog): log.info("Cog is ready!") self.ready.set() - @staticmethod - def is_dormant_message(message: t.Optional[discord.Message]) -> bool: + def is_dormant_message(self, message: t.Optional[discord.Message]) -> bool: """Return True if the contents of the `message` match `DORMANT_MSG`.""" if not message or not message.embeds: return False embed = message.embeds[0] - return embed.description.strip() == DORMANT_MSG.strip() + return message.author == self.bot.user and embed.description.strip() == DORMANT_MSG.strip() async def move_idle_channel(self, channel: discord.TextChannel, has_task: bool = True) -> None: """ -- cgit v1.2.3 From 65aba04130533154cf9cb3ebb64414f41d987305 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 9 Apr 2020 17:48:31 +0200 Subject: Reverse order of moving to dormant and task cancellation. Reversing the order ensures the task is not cancelled when moving to dormant fails which gives a fallback to move it after the initial period of time. --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 5f59a9d60..69812eda0 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -217,8 +217,8 @@ class HelpChannels(Scheduler, commands.Cog): with suppress(discord.errors.HTTPException, discord.errors.NotFound): await self.reset_claimant_send_permission(ctx.channel) - self.cancel_task(ctx.channel.id) await self.move_to_dormant(ctx.channel) + self.cancel_task(ctx.channel.id) else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") -- cgit v1.2.3 From d0a9404cb0cfee64bf26df5ec7cce2ea71cb4f15 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 9 Apr 2020 18:02:30 +0200 Subject: Delete channel from claimant cache. Deleting the channel from the claimant cache on invokation of the dormant command prevents users running the command multiple times before the bot moves it. --- bot/cogs/help_channels.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 69812eda0..346d35aa8 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -210,6 +210,9 @@ class HelpChannels(Scheduler, commands.Cog): log.trace("dormant command invoked; checking if the channel is in-use.") if ctx.channel.category == self.in_use_category: if await self.dormant_check(ctx): + with suppress(KeyError): + del self.help_channel_claimants[ctx.channel] + with suppress(discord.errors.NotFound): log.trace("Deleting dormant invokation message.") await ctx.message.delete() -- cgit v1.2.3 From 4ac17f806c2f9d98503067dbd181ae2a839bc74c Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 9 Apr 2020 18:31:55 +0200 Subject: Specify encoding to logging file handler. With the new addition of non latin-11 chars in channel names - which get logged, the logging to files fails on those entries on OSs where the default encoding is not utf8 or an other encoding capable of handling them. --- bot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/__init__.py b/bot/__init__.py index c9dbc3f40..2dd4af225 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -33,7 +33,7 @@ log_format = logging.Formatter(format_string) log_file = Path("logs", "bot.log") log_file.parent.mkdir(exist_ok=True) -file_handler = handlers.RotatingFileHandler(log_file, maxBytes=5242880, backupCount=7) +file_handler = handlers.RotatingFileHandler(log_file, maxBytes=5242880, backupCount=7, encoding="utf8") file_handler.setFormatter(log_format) root_log = logging.getLogger() -- cgit v1.2.3 From 3eecb147d6f788c83b230e9f79edf44da4d5c621 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Fri, 10 Apr 2020 15:38:19 +0200 Subject: Use synchronized permission reset. --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 346d35aa8..daadcc9a4 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -635,7 +635,7 @@ class HelpChannels(Scheduler, commands.Cog): return log.trace(f"Resetting send permissions for {member} ({member.id}).") - await self.available_category.set_permissions(member, overwrite=None) + await self.update_category_permissions(self.available_category, member, overwrite=None) # Ignore missing task when claim cooldown has passed but the channel still isn't dormant. self.cancel_task(member.id, ignore_missing=True) -- cgit v1.2.3 From d8e54d9921d204a0a95118136982186c790e0dd8 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Fri, 10 Apr 2020 15:43:34 +0200 Subject: Fix `help_channel_claimants` typehint. `ctx.author` that is used to populate the dict returns a `Member` object in most cases while only `User` was documented as a possible value. --- bot/cogs/help_channels.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index daadcc9a4..4404ecc17 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -109,7 +109,9 @@ class HelpChannels(Scheduler, commands.Cog): super().__init__() self.bot = bot - self.help_channel_claimants: t.Dict[discord.TextChannel, discord.User] = {} + self.help_channel_claimants: ( + t.Dict[discord.TextChannel, t.Union[discord.Member, discord.User]] + ) = {} # Categories self.available_category: discord.CategoryChannel = None -- cgit v1.2.3 From ef555490e222474a48fa470f30a1e600816c465f Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 11 Apr 2020 18:14:07 +0100 Subject: StatsD integration --- Pipfile | 3 ++- Pipfile.lock | 59 ++++++++++++++++++++-------------------- bot/__main__.py | 1 + bot/bot.py | 14 ++++++++-- bot/cogs/antispam.py | 1 + bot/cogs/defcon.py | 1 + bot/cogs/error_handler.py | 2 ++ bot/cogs/filtering.py | 2 ++ bot/cogs/help_channels.py | 21 +++++++++++++-- bot/cogs/stats.py | 65 +++++++++++++++++++++++++++++++++++++++++++++ bot/cogs/tags.py | 3 +++ bot/cogs/token_remover.py | 2 ++ bot/cogs/webhook_remover.py | 2 ++ bot/constants.py | 1 + config-default.yml | 2 ++ 15 files changed, 145 insertions(+), 34 deletions(-) create mode 100644 bot/cogs/stats.py diff --git a/Pipfile b/Pipfile index 04cc98427..e7fb61957 100644 --- a/Pipfile +++ b/Pipfile @@ -19,7 +19,8 @@ requests = "~=2.22" more_itertools = "~=8.2" sentry-sdk = "~=0.14" coloredlogs = "~=14.0" -colorama = {version = "~=0.4.3", sys_platform = "== 'win32'"} +colorama = {version = "~=0.4.3",sys_platform = "== 'win32'"} +statsd = "~=3.3" [dev-packages] coverage = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index ad9a3173a..19e03bda4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2d3ba484e8467a115126b2ba39fa5f36f103ea455477813dd658797875c79cc9" + "sha256": "10636aef5a07f17bd00608df2cc5214fcbfe3de4745cdeea7a076b871754620a" }, "pipfile-spec": 6, "requires": { @@ -87,18 +87,18 @@ }, "beautifulsoup4": { "hashes": [ - "sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a", - "sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887", - "sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae" + "sha256:594ca51a10d2b3443cbac41214e12dbb2a1cd57e1a7344659849e2e20ba6a8d8", + "sha256:a4bbe77fd30670455c5296242967a123ec28c37e9702a8a81bd2f20a4baf0368", + "sha256:d4e96ac9b0c3a6d3f0caae2e4124e6055c5dcafde8e2f831ff194c104f0775a0" ], - "version": "==4.8.2" + "version": "==4.9.0" }, "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" ], - "version": "==2019.11.28" + "version": "==2020.4.5.1" }, "cffi": { "hashes": [ @@ -167,10 +167,10 @@ }, "discord-py": { "hashes": [ - "sha256:7424be26b07b37ecad4404d9383d685995a0e0b3df3f9c645bdd3a4d977b83b4" + "sha256:406871b06d86c3dc49fba63238519f28628dac946fef8a0e22988ff58ec05580" ], "index": "pypi", - "version": "==1.3.2" + "version": "==1.3.3" }, "docutils": { "hashes": [ @@ -393,17 +393,10 @@ }, "pyparsing": { "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "version": "==2.4.6" - }, - "pyreadline": { - "hashes": [ - "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1" - ], - "markers": "sys_platform == 'win32'", - "version": "==2.1" + "version": "==2.4.7" }, "python-dateutil": { "hashes": [ @@ -524,6 +517,14 @@ ], "version": "==1.1.4" }, + "statsd": { + "hashes": [ + "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa", + "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f" + ], + "index": "pypi", + "version": "==3.3.0" + }, "urllib3": { "hashes": [ "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", @@ -717,11 +718,11 @@ }, "flake8-tidy-imports": { "hashes": [ - "sha256:5b6e75cec6d751e66534c522fbdce7dac1c2738b1216b0f6b10453995932e188", - "sha256:cf26fbb3ab31a398f265d53b6f711d80006450c19221e41b2b7b0e0b14ac39c5" + "sha256:62059ca07d8a4926b561d392cbab7f09ee042350214a25cf12823384a45d27dd", + "sha256:c30b40337a2e6802ba3bb611c26611154a27e94c53fc45639e3e282169574fd3" ], "index": "pypi", - "version": "==4.0.1" + "version": "==4.1.0" }, "flake8-todo": { "hashes": [ @@ -732,10 +733,10 @@ }, "identify": { "hashes": [ - "sha256:a7577a1f55cee1d21953a5cf11a3c839ab87f5ef909a4cba6cf52ed72b4c6059", - "sha256:ab246293e6585a1c6361a505b68d5b501a0409310932b7de2c2ead667b564d89" + "sha256:2bb8760d97d8df4408f4e805883dad26a2d076f04be92a10a3e43f09c6060742", + "sha256:faffea0fd8ec86bb146ac538ac350ed0c73908326426d387eded0bcc9d077522" ], - "version": "==1.4.13" + "version": "==1.4.14" }, "mccabe": { "hashes": [ @@ -835,10 +836,10 @@ }, "virtualenv": { "hashes": [ - "sha256:87831f1070534b636fea2241dd66f3afe37ac9041bcca6d0af3215cdcfbf7d82", - "sha256:f3128d882383c503003130389bf892856341c1da12c881ae24d6358c82561b55" + "sha256:00cfe8605fb97f5a59d52baab78e6070e72c12ca64f51151695407cc0eb8a431", + "sha256:c8364ec469084046c779c9a11ae6340094e8a0bf1d844330fc55c1cefe67c172" ], - "version": "==20.0.13" + "version": "==20.0.17" } } } diff --git a/bot/__main__.py b/bot/__main__.py index bf98f2cfd..2125e8590 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -57,6 +57,7 @@ bot.load_extension("bot.cogs.reminders") bot.load_extension("bot.cogs.site") bot.load_extension("bot.cogs.snekbox") bot.load_extension("bot.cogs.sync") +bot.load_extension("bot.cogs.stats") bot.load_extension("bot.cogs.tags") bot.load_extension("bot.cogs.token_remover") bot.load_extension("bot.cogs.utils") diff --git a/bot/bot.py b/bot/bot.py index 950ac6751..65081e438 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -6,10 +6,10 @@ from typing import Optional import aiohttp import discord +import statsd from discord.ext import commands -from bot import api -from bot import constants +from bot import DEBUG_MODE, api, constants log = logging.getLogger('bot') @@ -33,6 +33,16 @@ class Bot(commands.Bot): self._resolver = None self._guild_available = asyncio.Event() + statsd_url = constants.Bot.statsd_host + + if DEBUG_MODE: + # Since statsd is UDP, there are no errors for sending to a down port. + # For this reason, setting the statsd host to 127.0.0.1 for development + # will effectively disable stats. + statsd_url = "127.0.0.1" + + self.stats = statsd.StatsClient(statsd_url, 8125, prefix="bot") + def add_cog(self, cog: commands.Cog) -> None: """Adds a "cog" to the bot and logs the operation.""" super().add_cog(cog) diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py index baa6b9459..d63acbc4a 100644 --- a/bot/cogs/antispam.py +++ b/bot/cogs/antispam.py @@ -182,6 +182,7 @@ class AntiSpam(Cog): # which contains the reason for why the message violated the rule and # an iterable of all members that violated the rule. if result is not None: + self.bot.stats.incr(f"mod_alerts.{rule_name}") reason, members, relevant_messages = result full_reason = f"`{rule_name}` rule: {reason}" diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index cc0f79fe8..80dc6082f 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -104,6 +104,7 @@ class Defcon(Cog): log.exception(f"Unable to send rejection message to user: {member}") await member.kick(reason="DEFCON active, user is too new") + self.bot.stats.incr("defcon_leaves") message = ( f"{member} (`{member.id}`) was denied entry because their account is too new." diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 6a622d2ce..747ab4a6e 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -236,6 +236,8 @@ class ErrorHandler(Cog): f"```{e.__class__.__name__}: {e}```" ) + ctx.bot.stats.incr("command_error_count") + with push_scope() as scope: scope.user = { "id": ctx.author.id, diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index 3f3dbb853..fa4420be1 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -207,6 +207,8 @@ class Filtering(Cog): log.debug(message) + self.bot.stats.incr(f"bot.filters.{filter_name}") + additional_embeds = None additional_embeds_msg = None diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 697a4d3b7..389a4ad2a 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -367,6 +367,18 @@ class HelpChannels(Scheduler, commands.Cog): log.info("Cog is ready!") self.ready.set() + self.report_stats() + + def report_stats(self) -> None: + """Report the channel count stats.""" + total_in_use = len(list(self.get_category_channels(self.in_use_category))) + total_available = len(list(self.get_category_channels(self.available_category))) + total_dormant = len(list(self.get_category_channels(self.dormant_category))) + + self.bot.stats.gauge("help.total.in_use", total_in_use) + self.bot.stats.gauge("help.total.available", total_available) + self.bot.stats.gauge("help.total.dormant", total_dormant) + @staticmethod def is_dormant_message(message: t.Optional[discord.Message]) -> bool: """Return True if the contents of the `message` match `DORMANT_MSG`.""" @@ -432,6 +444,7 @@ class HelpChannels(Scheduler, commands.Cog): f"synchronized permissions after moving `{channel}` into it." ) await self.ensure_permissions_synchronization(self.available_category) + self.report_stats() async def move_to_dormant(self, channel: discord.TextChannel) -> None: """Make the `channel` dormant.""" @@ -442,7 +455,6 @@ class HelpChannels(Scheduler, commands.Cog): category=self.dormant_category, sync_permissions=True, topic=DORMANT_TOPIC, - position=10000, ) log.trace(f"Position of #{channel} ({channel.id}) is actually {channel.position}.") @@ -453,6 +465,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Pushing #{channel} ({channel.id}) into the channel queue.") self.channel_queue.put_nowait(channel) + self.report_stats() async def move_to_in_use(self, channel: discord.TextChannel) -> None: """Make a channel in-use and schedule it to be made dormant.""" @@ -463,7 +476,6 @@ class HelpChannels(Scheduler, commands.Cog): category=self.in_use_category, sync_permissions=True, topic=IN_USE_TOPIC, - position=10000, ) timeout = constants.HelpChannels.idle_minutes * 60 @@ -471,6 +483,7 @@ class HelpChannels(Scheduler, commands.Cog): log.trace(f"Scheduling #{channel} ({channel.id}) to become dormant in {timeout} sec.") data = TaskData(timeout, self.move_idle_channel(channel)) self.schedule_task(channel.id, data) + self.report_stats() async def notify(self) -> None: """ @@ -511,6 +524,8 @@ class HelpChannels(Scheduler, commands.Cog): f"using the `{constants.Bot.prefix}dormant` command within the channels." ) + self.bot.stats.incr("help.out_of_channel_alerts") + self.last_notification = message.created_at except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. @@ -543,6 +558,8 @@ class HelpChannels(Scheduler, commands.Cog): await self.move_to_in_use(channel) await self.revoke_send_permissions(message.author) + self.bot.stats.incr("help.claimed") + log.trace(f"Releasing on_message lock for {message.id}.") # Move a dormant channel to the Available category to fill in the gap. diff --git a/bot/cogs/stats.py b/bot/cogs/stats.py new file mode 100644 index 000000000..b75d29b7e --- /dev/null +++ b/bot/cogs/stats.py @@ -0,0 +1,65 @@ +from discord import Member, Message, Status +from discord.ext.commands import Bot, Cog, Context + + +class Stats(Cog): + """A cog which provides a way to hook onto Discord events and forward to stats.""" + + def __init__(self, bot: Bot): + self.bot = bot + + @Cog.listener() + async def on_message(self, message: Message) -> None: + """Report message events in the server to statsd.""" + if message.guild is None: + return + + reformatted_name = message.channel.name.replace('-', '_') + + if reformatted_name.startswith("ot"): + # Off-topic channels change names, we don't want this for stats. + # This will change 'ot1-lemon-in-the-dishwasher' to just 'ot1' + reformatted_name = reformatted_name[:3] + + stat_name = f"channels.{reformatted_name}" + self.bot.stats.incr(stat_name) + + # Increment the total message count + self.bot.stats.incr("messages") + + @Cog.listener() + async def on_command_completion(self, ctx: Context) -> None: + """Report completed commands to statsd.""" + command_name = ctx.command.qualified_name.replace(" ", "_") + + self.bot.stats.incr(f"commands.{command_name}") + + @Cog.listener() + async def on_member_join(self, member: Member) -> None: + """Update member count stat on member join.""" + self.bot.stats.gauge(f"guild.total_members", len(member.guild.members)) + + @Cog.listener() + async def on_member_leave(self, member: Member) -> None: + """Update member count stat on member leave.""" + self.bot.stats.gauge(f"guild.total_members", len(member.guild.members)) + + @Cog.listener() + async def on_member_update(self, _before: Member, after: Member) -> None: + """Update presence estimates on member update.""" + members = after.guild.members + + online = len([m for m in members if m.status == Status.online]) + idle = len([m for m in members if m.status == Status.idle]) + dnd = len([m for m in members if m.status == Status.do_not_disturb]) + offline = len([m for m in members if m.status == Status.offline]) + + self.bot.stats.gauge("guild.status.online", online) + self.bot.stats.gauge("guild.status.idle", idle) + self.bot.stats.gauge("guild.status.do_not_disturb", dnd) + self.bot.stats.gauge("guild.status.offline", offline) + + +def setup(bot: Bot) -> None: + """Load the stats cog.""" + bot.add_cog(Stats(bot)) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index a6e5952ff..b81859db1 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -207,6 +207,9 @@ class Tags(Cog): "time": time.time(), "channel": ctx.channel.id } + + self.bot.stats.incr(f"tags.usages.{tag_name.replace('-', '_')}") + await wait_for_deletion( await ctx.send(embed=Embed.from_dict(tag['embed'])), [ctx.author.id], diff --git a/bot/cogs/token_remover.py b/bot/cogs/token_remover.py index 421ad23e2..6721f0e02 100644 --- a/bot/cogs/token_remover.py +++ b/bot/cogs/token_remover.py @@ -93,6 +93,8 @@ class TokenRemover(Cog): channel_id=Channels.mod_alerts, ) + self.bot.stats.incr("tokens.removed_tokens") + @classmethod def find_token_in_message(cls, msg: Message) -> t.Optional[str]: """Return a seemingly valid token found in `msg` or `None` if no token is found.""" diff --git a/bot/cogs/webhook_remover.py b/bot/cogs/webhook_remover.py index 49692113d..1b5c3f821 100644 --- a/bot/cogs/webhook_remover.py +++ b/bot/cogs/webhook_remover.py @@ -54,6 +54,8 @@ class WebhookRemover(Cog): channel_id=Channels.mod_alerts ) + self.bot.stats.incr("tokens.removed_webhooks") + @Cog.listener() async def on_message(self, msg: Message) -> None: """Check if a Discord webhook URL is in `message`.""" diff --git a/bot/constants.py b/bot/constants.py index 60e3c4897..33c1d530d 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -199,6 +199,7 @@ class Bot(metaclass=YAMLGetter): prefix: str token: str sentry_dsn: str + statsd_host: str class Filter(metaclass=YAMLGetter): section = "filter" diff --git a/config-default.yml b/config-default.yml index 896003973..567caacbf 100644 --- a/config-default.yml +++ b/config-default.yml @@ -3,6 +3,8 @@ bot: token: !ENV "BOT_TOKEN" sentry_dsn: !ENV "BOT_SENTRY_DSN" + statsd_host: "graphite" + cooldowns: # Per channel, per tag. tags: 60 -- cgit v1.2.3 From bc17ec3f60b061ce93f627c1f69182f655d4fc37 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 11 Apr 2020 19:18:31 +0100 Subject: Address review comments from Mark --- bot.svg | 3795 +++++++++++++++++++++++++++++++++++++++++++++ bot/__main__.py | 4 +- bot/cogs/defcon.py | 2 +- bot/cogs/error_handler.py | 2 +- bot/cogs/filtering.py | 2 +- bot/cogs/stats.py | 46 +- 6 files changed, 3837 insertions(+), 14 deletions(-) create mode 100644 bot.svg diff --git a/bot.svg b/bot.svg new file mode 100644 index 000000000..97a3914d4 --- /dev/null +++ b/bot.svg @@ -0,0 +1,3795 @@ + + + + + + +G + + + +bot_cogs_clean + +bot.cogs.clean + + + +requests + +requests + + + +discord_Webhook + +discord. +Webhook + + + +requests->discord_Webhook + + + + + +bot_cogs_doc + +bot.cogs.doc + + + +requests->bot_cogs_doc + + + + + +bot_bot + +bot.bot + + + +bot_bot->bot_cogs_clean + + + + + +bot_cogs_moderation_management + +bot. +cogs. +moderation. +management + + + +bot_bot->bot_cogs_moderation_management + + + + + +bot_cogs_webhook_remover + +bot. +cogs. +webhook_remover + + + +bot_bot->bot_cogs_webhook_remover + + + + + + + +bot_cogs_verification + +bot. +cogs. +verification + + + +bot_bot->bot_cogs_verification + + + + + +bot_cogs_wolfram + +bot. +cogs. +wolfram + + + +bot_bot->bot_cogs_wolfram + + + + + + +bot_cogs_site + +bot.cogs.site + + + +bot_bot->bot_cogs_site + + + + + +bot_cogs_watchchannels_watchchannel + +bot. +cogs. +watchchannels. +watchchannel + + + +bot_bot->bot_cogs_watchchannels_watchchannel + + + + + + +bot_cogs_extensions + +bot. +cogs. +extensions + + + +bot_bot->bot_cogs_extensions + + + + + +bot_cogs_antimalware + +bot. +cogs. +antimalware + + + +bot_bot->bot_cogs_antimalware + + + + + +bot___main__ + +bot.__main__ + + + +bot_bot->bot___main__ + + + + + +bot_cogs_moderation + +bot. +cogs. +moderation + + + +bot_bot->bot_cogs_moderation + + + + + +bot_cogs_duck_pond + +bot. +cogs. +duck_pond + + + +bot_bot->bot_cogs_duck_pond + + + + + + +bot_cogs_antispam + +bot. +cogs. +antispam + + + +bot_bot->bot_cogs_antispam + + + + + +bot_interpreter + +bot. +interpreter + + + +bot_bot->bot_interpreter + + + + + +bot_cogs_moderation_superstarify + +bot. +cogs. +moderation. +superstarify + + + +bot_bot->bot_cogs_moderation_superstarify + + + + +bot_cogs_defcon + +bot. +cogs. +defcon + + + +bot_bot->bot_cogs_defcon + + + + + + +bot_cogs_moderation_ModLog + +bot. +cogs. +moderation. +ModLog + + + +bot_bot->bot_cogs_moderation_ModLog + + + + + +bot_cogs_watchchannels_talentpool + +bot. +cogs. +watchchannels. +talentpool + + + +bot_bot->bot_cogs_watchchannels_talentpool + + + + +bot_cogs_information + +bot. +cogs. +information + + + +bot_bot->bot_cogs_information + + + + + +bot_cogs_off_topic_names + +bot. +cogs. +off_topic_names + + + +bot_bot->bot_cogs_off_topic_names + + + + + + +bot_cogs_alias + +bot.cogs.alias + + + +bot_bot->bot_cogs_alias + + + + + + +bot_cogs_moderation_silence + +bot. +cogs. +moderation. +silence + + + +bot_bot->bot_cogs_moderation_silence + + + + + +bot_cogs_sync_syncers + +bot. +cogs. +sync. +syncers + + + +bot_bot->bot_cogs_sync_syncers + + + + + + +bot_cogs_moderation_scheduler + +bot. +cogs. +moderation. +scheduler + + + +bot_bot->bot_cogs_moderation_scheduler + + + + + + +bot_cogs_eval + +bot.cogs.eval + + + +bot_bot->bot_cogs_eval + + + + + +bot_cogs_help_channels + +bot. +cogs. +help_channels + + + +bot_bot->bot_cogs_help_channels + + + + + +bot_cogs_tags + +bot.cogs.tags + + + +bot_bot->bot_cogs_tags + + + + + + +bot_cogs_reddit + +bot. +cogs. +reddit + + + +bot_bot->bot_cogs_reddit + + + + + + +bot_cogs_utils + +bot.cogs.utils + + + +bot_bot->bot_cogs_utils + + + + + +bot_cogs_sync + +bot.cogs.sync + + + +bot_bot->bot_cogs_sync + + + + + +bot_cogs_help + +bot.cogs.help + + + +bot_bot->bot_cogs_help + + + + + +bot_cogs_bot + +bot.cogs.bot + + + +bot_bot->bot_cogs_bot + + + + + + + + +bot_cogs_watchchannels_bigbrother + +bot. +cogs. +watchchannels. +bigbrother + + + +bot_bot->bot_cogs_watchchannels_bigbrother + + + + +bot_cogs_logging + +bot. +cogs. +logging + + + +bot_bot->bot_cogs_logging + + + + + + +bot_cogs_config_verifier + +bot. +cogs. +config_verifier + + + +bot_bot->bot_cogs_config_verifier + + + + + + +bot_cogs_moderation_modlog + +bot. +cogs. +moderation. +modlog + + + +bot_bot->bot_cogs_moderation_modlog + + + + + + +bot_cogs_sync_cog + +bot. +cogs. +sync. +cog + + + +bot_bot->bot_cogs_sync_cog + + + + + + +bot_cogs_snekbox + +bot. +cogs. +snekbox + + + +bot_bot->bot_cogs_snekbox + + + + + +bot_cogs_moderation_infractions + +bot. +cogs. +moderation. +infractions + + + +bot_bot->bot_cogs_moderation_infractions + + + + + +bot_cogs_watchchannels + +bot. +cogs. +watchchannels + + + +bot_bot->bot_cogs_watchchannels + + + + + +bot_bot->bot_cogs_doc + + + + + +bot_cogs_security + +bot. +cogs. +security + + + +bot_bot->bot_cogs_security + + + + + +bot_cogs_reminders + +bot. +cogs. +reminders + + + +bot_bot->bot_cogs_reminders + + + + + +bot_cogs_token_remover + +bot. +cogs. +token_remover + + + +bot_bot->bot_cogs_token_remover + + + + + + +bot_cogs_filtering + +bot. +cogs. +filtering + + + +bot_bot->bot_cogs_filtering + + + + + +bot_cogs_jams + +bot.cogs.jams + + + +bot_bot->bot_cogs_jams + + + + + +bot_cogs_error_handler + +bot. +cogs. +error_handler + + + +bot_bot->bot_cogs_error_handler + + + + + +bot_rules_attachments + +bot. +rules. +attachments + + + +bot_rules + +bot.rules + + + +bot_rules_attachments->bot_rules + + + + + +bot_cogs_stats + +bot.cogs.stats + + + +bot_api + +bot.api + + + +bot_api->bot_bot + + + + + +bot_api->bot_cogs_watchchannels_watchchannel + + + + +bot_cogs_moderation_utils + +bot. +cogs. +moderation. +utils + + + +bot_api->bot_cogs_moderation_utils + + + + + +bot_api->bot_cogs_watchchannels_talentpool + + + + +bot_api->bot_cogs_off_topic_names + + + + + +bot_api->bot_cogs_sync_syncers + + + + + +bot_api->bot_cogs_moderation_scheduler + + + + +bot_api->bot_cogs_sync_cog + + + + + +bot_api->bot_cogs_error_handler + + + + + + + +bot_cogs_moderation_management->bot_cogs_moderation + + + + + +urllib3_exceptions + +urllib3. +exceptions + + + +urllib3_exceptions->requests + + + + + +urllib3 + +urllib3 + + + +urllib3_exceptions->urllib3 + + + + + +urllib3_exceptions->bot_cogs_doc + + + + + +dateutil_parser + +dateutil. +parser + + + +bot_converters + +bot.converters + + + +dateutil_parser->bot_converters + + + + + +dateutil_parser->bot_cogs_watchchannels_watchchannel + + + + + + +bot_utils_time + +bot.utils.time + + + +dateutil_parser->bot_utils_time + + + + + +dateutil_parser->bot_cogs_moderation_scheduler + + + + + + +dateutil_parser->bot_cogs_reminders + + + + +dateutil_relativedelta + +dateutil. +relativedelta + + + +dateutil_relativedelta->bot_cogs_wolfram + + + + + + +dateutil_relativedelta->bot_converters + + + + + + +dateutil_relativedelta->bot_cogs_moderation_ModLog + + + + + + +dateutil_relativedelta->bot_utils_time + + + + + +dateutil_relativedelta->bot_cogs_utils + + + + + +dateutil_relativedelta->bot_cogs_moderation_modlog + + + + + +dateutil_relativedelta->bot_cogs_reminders + + + + +dateutil_relativedelta->bot_cogs_filtering + + + + + + +bot_rules_chars + +bot. +rules. +chars + + + +bot_rules_chars->bot_rules + + + + + +discord_Guild + +discord.Guild + + + +discord_Guild->bot_cogs_sync_syncers + + + + + +bot_converters->bot_cogs_moderation_management + + + + +bot_converters->bot_cogs_antispam + + + + + +bot_converters->bot_cogs_moderation_superstarify + + + + +bot_converters->bot_cogs_watchchannels_talentpool + + + + + + +bot_converters->bot_cogs_alias + + + + + +bot_converters->bot_cogs_moderation_silence + + + + +bot_converters->bot_cogs_tags + + + + + +bot_converters->bot_cogs_reddit + + + + + +bot_converters->bot_cogs_watchchannels_bigbrother + + + + +bot_converters->bot_cogs_moderation_infractions + + + + + +bot_converters->bot_cogs_doc + + + + + +bot_converters->bot_cogs_reminders + + + + +bot_converters->bot_cogs_error_handler + + + + + +bs4 + +bs4 + + + +bs4->bot_cogs_doc + + + + + +discord_Client + +discord.Client + + + +bot_utils_messages + +bot. +utils. +messages + + + +discord_Client->bot_utils_messages + + + + +bot_rules_burst_shared + +bot. +rules. +burst_shared + + + +bot_rules_burst_shared->bot_rules + + + + + +bot_cogs_watchchannels_watchchannel->bot_cogs_watchchannels_talentpool + + + + + +bot_cogs_watchchannels_watchchannel->bot_cogs_watchchannels_bigbrother + + + + + +aiohttp + +aiohttp + + + +aiohttp->bot_bot + + + + + +aiohttp->bot_api + + + + + + +aiohttp->bot_converters + + + + + +aiohttp->discord_Client + + + + + + +aiohttp->discord_Webhook + + + + + +aiohttp->bot_cogs_reddit + + + + + +bot_cogs_extensions->bot_cogs_alias + + + + + +discord_User + +discord.User + + + +discord_User->bot_cogs_clean + + + + + + +discord_User->bot_cogs_duck_pond + + + + + + + +discord_User->bot_cogs_sync_syncers + + + + + +discord_User->bot_cogs_help + + + + + +discord_User->bot_cogs_sync_cog + + + + + +discord_User->bot_cogs_snekbox + + + + + +bot_rules->bot_cogs_antispam + + + + + +bot_cogs_moderation->bot_cogs_clean + + + + + +bot_cogs_moderation->bot_cogs_webhook_remover + + + + + +bot_cogs_moderation->bot_cogs_verification + + + + + +bot_cogs_moderation->bot_cogs_watchchannels_watchchannel + + + + + +bot_cogs_moderation->bot_cogs_antispam + + + + + +bot_cogs_moderation->bot_cogs_defcon + + + + + +bot_cogs_moderation->bot_cogs_watchchannels_bigbrother + + + + +bot_cogs_moderation->bot_cogs_token_remover + + + + + +bot_cogs_moderation->bot_cogs_filtering + + + + + +discord + +discord + + + +discord->bot_cogs_clean + + + + + +discord->bot_bot + + + + + +discord->bot_rules_attachments + + + + + +discord->bot_cogs_stats + + + + + +discord->bot_cogs_moderation_management + + + + + +discord->bot_cogs_webhook_remover + + + + +discord->bot_cogs_verification + + + + + + +discord->bot_cogs_wolfram + + + + + + + +discord->bot_rules_chars + + + + + +discord->bot_converters + + + + + +discord->bot_cogs_site + + + + + + +discord->bot_rules_burst_shared + + + + + +discord->bot_cogs_watchchannels_watchchannel + + + + + +discord->bot_cogs_extensions + + + + + +discord->bot_cogs_antimalware + + + + + +discord->bot___main__ + + + + + + +discord->bot_cogs_duck_pond + + + + + +discord->bot_cogs_antispam + + + + +discord->bot_interpreter + + + + + +discord->bot_cogs_moderation_superstarify + + + + + +discord->bot_cogs_defcon + + + + + + +bot_pagination + +bot.pagination + + + +discord->bot_pagination + + + + + +discord->bot_cogs_moderation_ModLog + + + + + + +discord->bot_cogs_moderation_utils + + + + + + +bot_rules_newlines + +bot. +rules. +newlines + + + +discord->bot_rules_newlines + + + + + + +discord->bot_cogs_watchchannels_talentpool + + + + + +discord->bot_cogs_information + + + + + +discord->bot_cogs_off_topic_names + + + + + +discord->bot_cogs_alias + + + + + +discord->bot_cogs_moderation_silence + + + + + + +discord->bot_cogs_sync_syncers + + + + + + +discord->bot_cogs_moderation_scheduler + + + + + + +discord->bot_cogs_eval + + + + + + +bot_rules_burst + +bot. +rules. +burst + + + +discord->bot_rules_burst + + + + + +discord->bot_cogs_help_channels + + + + + + +discord->bot_cogs_tags + + + + + +discord->bot_cogs_reddit + + + + + + +discord->bot_cogs_utils + + + + + +bot_rules_discord_emojis + +bot. +rules. +discord_emojis + + + +discord->bot_rules_discord_emojis + + + + + +bot_rules_duplicates + +bot. +rules. +duplicates + + + +discord->bot_rules_duplicates + + + + + +discord->bot_cogs_help + + + + + +discord->bot_cogs_bot + + + + + + + +discord->bot_cogs_logging + + + + + + +bot_rules_mentions + +bot. +rules. +mentions + + + +discord->bot_rules_mentions + + + + + +bot_decorators + +bot.decorators + + + +discord->bot_decorators + + + + + +discord->bot_cogs_moderation_modlog + + + + + +discord->bot_cogs_sync_cog + + + + + +discord->bot_cogs_snekbox + + + + + +discord->bot_cogs_moderation_infractions + + + + + + +bot_rules_links + +bot. +rules. +links + + + +discord->bot_rules_links + + + + + + +discord->bot_cogs_doc + + + + + +discord->bot_utils_messages + + + + + +bot_patches_message_edited_at + +bot. +patches. +message_edited_at + + + +discord->bot_patches_message_edited_at + + + + + + + +discord->bot_cogs_reminders + + + + + +bot_rules_role_mentions + +bot. +rules. +role_mentions + + + +discord->bot_rules_role_mentions + + + + + +discord->bot_cogs_token_remover + + + + + +discord->bot_cogs_filtering + + + + + +discord->bot_cogs_jams + + + + + +bot_constants + +bot.constants + + + +bot_constants->bot_cogs_clean + + + + + +bot_constants->bot_bot + + + + + +bot_constants->bot_api + + + + + +bot_constants->bot_cogs_moderation_management + + + + + +bot_constants->bot_cogs_webhook_remover + + + + +bot_constants->bot_cogs_verification + + + + + +bot_constants->bot_cogs_wolfram + + + + +bot_constants->bot_cogs_site + + + + + + +bot_constants->bot_cogs_watchchannels_watchchannel + + + + + +bot_constants->bot_cogs_extensions + + + + + +bot_constants->bot_cogs_antimalware + + + + + +bot_constants->bot___main__ + + + + + +bot_constants->bot_cogs_duck_pond + + + + + + +bot_constants->bot_cogs_antispam + + + + + +bot_constants->bot_cogs_moderation_superstarify + + + + +bot_constants->bot_cogs_defcon + + + + + +bot_constants->bot_pagination + + + + + + +bot_constants->bot_cogs_moderation_ModLog + + + + + + +bot_constants->bot_cogs_moderation_utils + + + + + +bot_constants->bot_cogs_watchchannels_talentpool + + + + + + +bot_constants->bot_cogs_information + + + + + + +bot_constants->bot_cogs_off_topic_names + + + + + +bot_constants->bot_cogs_moderation_silence + + + + + + +bot_constants->bot_cogs_sync_syncers + + + + + +bot_constants->bot_cogs_moderation_scheduler + + + + + + +bot_constants->bot_cogs_eval + + + + + +bot_constants->bot_cogs_help_channels + + + + + +bot_constants->bot_cogs_tags + + + + + + +bot_constants->bot_cogs_reddit + + + + +bot_constants->bot_cogs_utils + + + + + +bot_constants->bot_cogs_help + + + + + + + +bot_constants->bot_cogs_bot + + + + + +bot_constants->bot_cogs_watchchannels_bigbrother + + + + + + +bot_constants->bot_cogs_logging + + + + + + +bot_constants->bot_cogs_config_verifier + + + + + +bot_constants->bot_decorators + + + + +bot_constants->bot_cogs_moderation_modlog + + + + + + +bot_constants->bot_cogs_sync_cog + + + + + + +bot_constants->bot_cogs_snekbox + + + + + + +bot_constants->bot_cogs_moderation_infractions + + + + + + +bot_constants->bot_cogs_doc + + + + + +bot_constants->bot_utils_messages + + + + + + +bot_constants->bot_cogs_reminders + + + + +bot_constants->bot_cogs_token_remover + + + + + +bot_constants->bot_cogs_filtering + + + + + + + + +bot_constants->bot_cogs_jams + + + + + +bot_constants->bot_cogs_error_handler + + + + +discord_File + +discord.File + + + +discord_File->bot_utils_messages + + + + +discord_Reaction + +discord. +Reaction + + + +discord_Reaction->bot_cogs_sync_syncers + + + + + +discord_Reaction->bot_cogs_help + + + + + +discord_Reaction->bot_cogs_snekbox + + + + + +discord_Reaction->bot_utils_messages + + + + + +bot_interpreter->bot_cogs_eval + + + + + + +bot_cogs_moderation_superstarify->bot_cogs_moderation + + + + + + +more_itertools + +more_itertools + + + +more_itertools->bot_cogs_jams + + + + + +statsd + +statsd + + + +statsd->bot_bot + + + + + +bot_pagination->bot_cogs_moderation_management + + + + + + + +bot_pagination->bot_cogs_wolfram + + + + + +bot_pagination->bot_cogs_site + + + + + +bot_pagination->bot_cogs_watchchannels_watchchannel + + + + + +bot_pagination->bot_cogs_extensions + + + + + +bot_pagination->bot_cogs_watchchannels_talentpool + + + + + +bot_pagination->bot_cogs_information + + + + + +bot_pagination->bot_cogs_off_topic_names + + + + + + +bot_pagination->bot_cogs_alias + + + + + +bot_pagination->bot_cogs_tags + + + + + +bot_pagination->bot_cogs_reddit + + + + + + +bot_pagination->bot_cogs_help + + + + + +bot_pagination->bot_cogs_doc + + + + + +bot_pagination->bot_cogs_reminders + + + + + +bot_utils_checks + +bot. +utils. +checks + + + +bot_utils_checks->bot_cogs_moderation_management + + + + + +bot_utils_checks->bot_cogs_verification + + + + + +bot_utils_checks->bot_cogs_extensions + + + + + + +bot_utils_checks->bot_cogs_moderation_superstarify + + + + + +bot_utils_checks->bot_cogs_information + + + + + +bot_utils_checks->bot_cogs_moderation_silence + + + + + + +bot_utils_checks->bot_decorators + + + + + +bot_utils_checks->bot_cogs_moderation_infractions + + + + + + +bot_utils_checks->bot_cogs_reminders + + + + +bot_cogs_moderation_ModLog->bot_cogs_clean + + + + + + +bot_cogs_moderation_ModLog->bot_cogs_verification + + + + + +bot_cogs_moderation_ModLog->bot_cogs_watchchannels_watchchannel + + + + + +bot_cogs_moderation_ModLog->bot_cogs_antispam + + + + + +bot_cogs_moderation_ModLog->bot_cogs_defcon + + + + + + +bot_cogs_moderation_ModLog->bot_cogs_token_remover + + + + + + + +bot_cogs_moderation_ModLog->bot_cogs_filtering + + + + + + + +discord_Object + +discord.Object + + + +discord_Object->bot_cogs_verification + + + + + +discord_Object->bot_cogs_antispam + + + + + + +bot_cogs_moderation_utils->bot_cogs_moderation_management + + + + + +bot_cogs_moderation_utils->bot_cogs_moderation_superstarify + + + + + + +bot_cogs_moderation_utils->bot_cogs_moderation_scheduler + + + + + +bot_cogs_moderation_utils->bot_cogs_watchchannels_bigbrother + + + + + + + +bot_cogs_moderation_utils->bot_cogs_moderation_infractions + + + + + +discord_Webhook->bot_utils_messages + + + + + + + +bot_rules_newlines->bot_rules + + + + + +bot_cogs_watchchannels_talentpool->bot_cogs_watchchannels + + + + + +discord_utils + +discord.utils + + + +discord_utils->discord_Guild + + + + + +discord_utils->discord_Client + + + + +discord_utils->discord_User + + + + + +discord_utils->discord + + + + + +discord_utils->bot_cogs_moderation_ModLog + + + + + + +discord_utils->discord_Object + + + + + +discord_utils->discord_Webhook + + + + + +discord_utils->bot_cogs_information + + + + + + +discord_Message + +discord. +Message + + + +discord_utils->discord_Message + + + + + +discord_abc + +discord.abc + + + +discord_utils->discord_abc + + + + + +discord_message + +discord. +message + + + +discord_utils->discord_message + + + + + +discord_Role + +discord.Role + + + +discord_utils->discord_Role + + + + + +discord_Member + +discord.Member + + + +discord_utils->discord_Member + + + + + + +discord_utils->bot_cogs_moderation_modlog + + + + + +discord_utils->bot_patches_message_edited_at + + + + + +discord_utils->bot_cogs_token_remover + + + + + +discord_utils->bot_cogs_filtering + + + + + + +discord_utils->bot_cogs_jams + + + + + +bot_cogs_moderation_silence->bot_cogs_moderation + + + + + +bot_utils_time->bot_cogs_moderation_management + + + + + + +bot_utils_time->bot_cogs_wolfram + + + + + +bot_utils_time->bot_cogs_watchchannels_watchchannel + + + + + +bot_utils_time->bot_cogs_moderation_superstarify + + + + + +bot_utils_time->bot_cogs_moderation_ModLog + + + + + +bot_utils_time->bot_cogs_watchchannels_talentpool + + + + +bot_utils_time->bot_cogs_information + + + + + + +bot_utils_time->bot_cogs_moderation_scheduler + + + + + +bot_utils_time->bot_cogs_utils + + + + + +bot_utils_time->bot_cogs_moderation_modlog + + + + + + + +bot_utils_time->bot_cogs_reminders + + + + + +discord_Message->bot_cogs_clean + + + + + +discord_Message->bot_rules_attachments + + + + + +discord_Message->bot_cogs_stats + + + + + +discord_Message->bot_cogs_webhook_remover + + + + + + + +discord_Message->bot_cogs_verification + + + + + +discord_Message->bot_rules_chars + + + + + + +discord_Message->bot_rules_burst_shared + + + + + +discord_Message->bot_cogs_watchchannels_watchchannel + + + + + + +discord_Message->bot_cogs_antimalware + + + + + +discord_Message->bot_cogs_duck_pond + + + + + +discord_Message->bot_cogs_antispam + + + + + +discord_Message->bot_rules_newlines + + + + + + +discord_Message->bot_cogs_information + + + + + +discord_Message->bot_cogs_sync_syncers + + + + + +discord_Message->bot_rules_burst + + + + + + + +discord_Message->bot_cogs_utils + + + + + +discord_Message->bot_rules_discord_emojis + + + + + + +discord_Message->bot_rules_duplicates + + + + + + +discord_Message->bot_cogs_help + + + + + + + + +discord_Message->bot_cogs_bot + + + + + + +discord_Message->bot_rules_mentions + + + + + + +discord_Message->bot_cogs_snekbox + + + + + + + +discord_Message->bot_rules_links + + + + + + +discord_Message->bot_utils_messages + + + + + +discord_Message->bot_rules_role_mentions + + + + + +discord_Message->bot_cogs_token_remover + + + + + + +discord_Message->bot_cogs_filtering + + + + + + + +bot_cogs_sync_syncers->bot_cogs_sync_cog + + + + + +dateutil + +dateutil + + + +dateutil->bot_cogs_wolfram + + + + + +dateutil->bot_converters + + + + + + + +dateutil->bot_cogs_watchchannels_watchchannel + + + + +dateutil->bot_cogs_moderation_ModLog + + + + + +dateutil->bot_utils_time + + + + + +dateutil->bot_cogs_moderation_scheduler + + + + + +dateutil->bot_cogs_utils + + + + + +dateutil->bot_cogs_moderation_modlog + + + + + +dateutil->bot_cogs_reminders + + + + +dateutil->bot_cogs_filtering + + + + + +bot_cogs_moderation_scheduler->bot_cogs_moderation_superstarify + + + + + +bot_cogs_moderation_scheduler->bot_cogs_moderation_infractions + + + + + +bot_rules_burst->bot_rules + + + + + +discord_abc->discord_User + + + + + +discord_abc->discord + + + + + +discord_abc->bot_pagination + + + + + +discord_abc->bot_cogs_moderation_ModLog + + + + + +discord_abc->discord_Member + + + + + +discord_abc->bot_cogs_moderation_modlog + + + + + +discord_abc->bot_utils_messages + + + + +discord_message->discord + + + + + +discord_message->discord_Webhook + + + + + +discord_message->bot_patches_message_edited_at + + + + + + +bot_rules_discord_emojis->bot_rules + + + + + +yaml + +yaml + + + +yaml->bot_constants + + + + + +bot_rules_duplicates->bot_rules + + + + + +urllib3->requests + + + + + +urllib3->bot_cogs_doc + + + + + + +discord_Role->bot_cogs_information + + + + + +discord_Role->bot_cogs_utils + + + + + +discord_Role->bot_cogs_sync_cog + + + + + +discord_Colour + +discord.Colour + + + +discord_Colour->bot_cogs_clean + + + + +discord_Colour->bot_cogs_webhook_remover + + + + + +discord_Colour->bot_cogs_verification + + + + + +discord_Colour->bot_cogs_site + + + + + + +discord_Colour->bot_cogs_extensions + + + + + +discord_Colour->bot_cogs_antispam + + + + + +discord_Colour->bot_cogs_moderation_superstarify + + + + + + +discord_Colour->bot_cogs_defcon + + + + + + +discord_Colour->bot_cogs_moderation_ModLog + + + + + + +discord_Colour->bot_cogs_information + + + + + +discord_Colour->bot_cogs_off_topic_names + + + + + +discord_Colour->bot_cogs_alias + + + + + +discord_Colour->bot_cogs_tags + + + + + +discord_Colour->bot_cogs_reddit + + + + + +discord_Colour->bot_cogs_utils + + + + + +discord_Colour->bot_cogs_help + + + + + +discord_Colour->bot_decorators + + + + + +discord_Colour->bot_cogs_moderation_modlog + + + + + + +discord_Colour->bot_cogs_token_remover + + + + + + +discord_Colour->bot_cogs_filtering + + + + + +bot_cogs_watchchannels_bigbrother->bot_cogs_watchchannels + + + + + +discord_Member->bot_rules_attachments + + + + + + + +discord_Member->bot_cogs_stats + + + + + +discord_Member->bot_rules_chars + + + + + +discord_Member->bot_rules_burst_shared + + + + + +discord_Member->bot_cogs_duck_pond + + + + + + +discord_Member->bot_cogs_antispam + + + + +discord_Member->bot_cogs_moderation_superstarify + + + + + +discord_Member->bot_cogs_defcon + + + + + +discord_Member->bot_rules_newlines + + + + + +discord_Member->bot_cogs_watchchannels_talentpool + + + + + + +discord_Member->bot_cogs_information + + + + + +discord_Member->bot_cogs_sync_syncers + + + + + +discord_Member->bot_rules_burst + + + + + +discord_Member->bot_rules_discord_emojis + + + + + +discord_Member->bot_rules_duplicates + + + + +discord_Member->bot_rules_mentions + + + + +discord_Member->bot_decorators + + + + + +discord_Member->bot_cogs_sync_cog + + + + + +discord_Member->bot_cogs_moderation_infractions + + + + + +discord_Member->bot_rules_links + + + + + +discord_Member->bot_utils_messages + + + + + + +discord_Member->bot_rules_role_mentions + + + + + +discord_Member->bot_cogs_filtering + + + + + + + +discord_Member->bot_cogs_jams + + + + + +bot_rules_mentions->bot_rules + + + + + +bot_decorators->bot_cogs_clean + + + + + + + + +bot_decorators->bot_cogs_verification + + + + +bot_decorators->bot_cogs_defcon + + + + + +bot_decorators->bot_cogs_watchchannels_talentpool + + + + + +bot_decorators->bot_cogs_information + + + + + +bot_decorators->bot_cogs_off_topic_names + + + + + +bot_decorators->bot_cogs_eval + + + + + +bot_decorators->bot_cogs_help_channels + + + + + + +bot_decorators->bot_cogs_reddit + + + + + +bot_decorators->bot_cogs_utils + + + + + +bot_decorators->bot_cogs_help + + + + + +bot_decorators->bot_cogs_bot + + + + +bot_decorators->bot_cogs_watchchannels_bigbrother + + + + + +bot_decorators->bot_cogs_snekbox + + + + + + +bot_decorators->bot_cogs_moderation_infractions + + + + + +bot_decorators->bot_cogs_doc + + + + + +bot_decorators->bot_cogs_jams + + + + + +bot_decorators->bot_cogs_error_handler + + + + + +bot_cogs_moderation_modlog->bot_cogs_moderation_management + + + + + + +bot_cogs_moderation_modlog->bot_cogs_webhook_remover + + + + + +bot_cogs_moderation_modlog->bot_cogs_moderation + + + + +bot_cogs_moderation_modlog->bot_cogs_moderation_scheduler + + + + + +bot_patches + +bot.patches + + + +bot_patches->bot___main__ + + + + + +bot_cogs_sync_cog->bot_cogs_sync + + + + + +bot_utils + +bot.utils + + + +bot_utils->bot_cogs_moderation_management + + + + +bot_utils->bot_cogs_verification + + + + + + +bot_utils->bot_cogs_wolfram + + + + + +bot_utils->bot_cogs_watchchannels_watchchannel + + + + + + +bot_utils->bot_cogs_extensions + + + + + +bot_utils->bot_cogs_duck_pond + + + + +bot_utils->bot_cogs_antispam + + + + + + +bot_utils->bot_cogs_moderation_superstarify + + + + + +bot_utils->bot_cogs_moderation_ModLog + + + + + + +bot_utils->bot_cogs_watchchannels_talentpool + + + + + + +bot_utils->bot_cogs_information + + + + +bot_utils->bot_cogs_moderation_silence + + + + + + + +bot_utils->bot_cogs_moderation_scheduler + + + + + +bot_utils->bot_cogs_help_channels + + + + + +bot_utils->bot_cogs_tags + + + + + +bot_utils->bot_cogs_utils + + + + + +bot_utils->bot_cogs_bot + + + + + + +bot_utils->bot_decorators + + + + + +bot_utils->bot_cogs_moderation_modlog + + + + + +bot_utils->bot_cogs_snekbox + + + + + +bot_utils->bot_cogs_moderation_infractions + + + + + + + +bot_utils->bot_cogs_reminders + + + + + + +bot_cogs_moderation_infractions->bot_cogs_moderation_management + + + + + +bot_cogs_moderation_infractions->bot_cogs_moderation + + + + + +bot_rules_links->bot_rules + + + + + +discord_errors + +discord.errors + + + +discord_errors->discord_Guild + + + + + +discord_errors->discord_Client + + + + + + +discord_errors->bot_cogs_watchchannels_watchchannel + + + + +discord_errors->discord_User + + + + + + +discord_errors->discord + + + + + +discord_errors->bot_cogs_duck_pond + + + + + +discord_errors->discord_Webhook + + + + + +discord_errors->discord_utils + + + + + +discord_errors->discord_Message + + + + + + +discord_errors->discord_abc + + + + + +discord_errors->discord_message + + + + + +discord_errors->discord_Role + + + + + + +discord_errors->bot_decorators + + + + + + +discord_errors->bot_cogs_doc + + + + + + +discord_errors->bot_utils_messages + + + + + +discord_errors->bot_cogs_filtering + + + + + +bot_utils_scheduling + +bot. +utils. +scheduling + + + +bot_utils_scheduling->bot_cogs_moderation_scheduler + + + + +bot_utils_scheduling->bot_cogs_help_channels + + + + + +bot_utils_scheduling->bot_cogs_reminders + + + + + +bs4_element + +bs4.element + + + +bs4_element->bs4 + + + + + +bs4_element->bot_cogs_doc + + + + + +dateutil_tz + +dateutil.tz + + + +dateutil_tz->bot_converters + + + + +bot_utils_messages->bot_cogs_watchchannels_watchchannel + + + + + + + +bot_utils_messages->bot_cogs_duck_pond + + + + + +bot_utils_messages->bot_cogs_antispam + + + + + + +bot_utils_messages->bot_cogs_tags + + + + + +bot_utils_messages->bot_cogs_bot + + + + +bot_utils_messages->bot_cogs_snekbox + + + + +bot_patches_message_edited_at->bot_patches + + + + + +bot_rules_role_mentions->bot_rules + + + + + +bot_cogs_token_remover->bot_cogs_bot + + + + + diff --git a/bot/__main__.py b/bot/__main__.py index 2125e8590..3aa36bfc0 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -46,8 +46,8 @@ bot.load_extension("bot.cogs.verification") # Feature cogs bot.load_extension("bot.cogs.alias") bot.load_extension("bot.cogs.defcon") -bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.duck_pond") +bot.load_extension("bot.cogs.eval") bot.load_extension("bot.cogs.information") bot.load_extension("bot.cogs.jams") bot.load_extension("bot.cogs.moderation") @@ -56,8 +56,8 @@ bot.load_extension("bot.cogs.reddit") bot.load_extension("bot.cogs.reminders") bot.load_extension("bot.cogs.site") bot.load_extension("bot.cogs.snekbox") -bot.load_extension("bot.cogs.sync") bot.load_extension("bot.cogs.stats") +bot.load_extension("bot.cogs.sync") bot.load_extension("bot.cogs.tags") bot.load_extension("bot.cogs.token_remover") bot.load_extension("bot.cogs.utils") diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 80dc6082f..06b2f25c6 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -104,7 +104,7 @@ class Defcon(Cog): log.exception(f"Unable to send rejection message to user: {member}") await member.kick(reason="DEFCON active, user is too new") - self.bot.stats.incr("defcon_leaves") + self.bot.stats.incr("defcon.leaves") message = ( f"{member} (`{member.id}`) was denied entry because their account is too new." diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 747ab4a6e..722376cc6 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -236,7 +236,7 @@ class ErrorHandler(Cog): f"```{e.__class__.__name__}: {e}```" ) - ctx.bot.stats.incr("command_error_count") + ctx.bot.stats.incr("errors.commands") with push_scope() as scope: scope.user = { diff --git a/bot/cogs/filtering.py b/bot/cogs/filtering.py index fa4420be1..6a703f5a1 100644 --- a/bot/cogs/filtering.py +++ b/bot/cogs/filtering.py @@ -207,7 +207,7 @@ class Filtering(Cog): log.debug(message) - self.bot.stats.incr(f"bot.filters.{filter_name}") + self.bot.stats.incr(f"filters.{filter_name}") additional_embeds = None additional_embeds_msg = None diff --git a/bot/cogs/stats.py b/bot/cogs/stats.py index b75d29b7e..e963dc312 100644 --- a/bot/cogs/stats.py +++ b/bot/cogs/stats.py @@ -1,6 +1,16 @@ from discord import Member, Message, Status from discord.ext.commands import Bot, Cog, Context +from bot.constants import Guild + + +CHANNEL_NAME_OVERRIDES = { + Guild.channels.off_topic_0: "off_topic_0", + Guild.channels.off_topic_1: "off_topic_1", + Guild.channels.off_topic_2: "off_topic_2", + Guild.channels.staff_lounge: "staff_lounge" +} + class Stats(Cog): """A cog which provides a way to hook onto Discord events and forward to stats.""" @@ -14,12 +24,13 @@ class Stats(Cog): if message.guild is None: return + if message.guild.id != Guild.id: + return + reformatted_name = message.channel.name.replace('-', '_') - if reformatted_name.startswith("ot"): - # Off-topic channels change names, we don't want this for stats. - # This will change 'ot1-lemon-in-the-dishwasher' to just 'ot1' - reformatted_name = reformatted_name[:3] + if CHANNEL_NAME_OVERRIDES.get(message.channel.id): + reformatted_name = CHANNEL_NAME_OVERRIDES.get(message.channel.id) stat_name = f"channels.{reformatted_name}" self.bot.stats.incr(stat_name) @@ -37,22 +48,39 @@ class Stats(Cog): @Cog.listener() async def on_member_join(self, member: Member) -> None: """Update member count stat on member join.""" + if member.guild.id != Guild.id: + return + self.bot.stats.gauge(f"guild.total_members", len(member.guild.members)) @Cog.listener() async def on_member_leave(self, member: Member) -> None: """Update member count stat on member leave.""" + if member.guild.id != Guild.id: + return + self.bot.stats.gauge(f"guild.total_members", len(member.guild.members)) @Cog.listener() async def on_member_update(self, _before: Member, after: Member) -> None: """Update presence estimates on member update.""" - members = after.guild.members + if after.guild.id != Guild.id: + return - online = len([m for m in members if m.status == Status.online]) - idle = len([m for m in members if m.status == Status.idle]) - dnd = len([m for m in members if m.status == Status.do_not_disturb]) - offline = len([m for m in members if m.status == Status.offline]) + online = 0 + idle = 0 + dnd = 0 + offline = 0 + + for member in after.guild.members: + if member.status == Status.online: + online += 1 + elif member.status == Status.dnd: + dnd += 1 + elif member.status == Status.idle: + idle += 1 + else: + offline += 1 self.bot.stats.gauge("guild.status.online", online) self.bot.stats.gauge("guild.status.idle", idle) -- cgit v1.2.3 From 77c07fc2bdad4d7caa9dc654f62a9b1c4dfb63b2 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 11 Apr 2020 19:24:54 +0100 Subject: Address review comments from Mark --- bot.svg | 3795 ----------------------------------------------------- bot/cogs/stats.py | 10 +- 2 files changed, 5 insertions(+), 3800 deletions(-) delete mode 100644 bot.svg diff --git a/bot.svg b/bot.svg deleted file mode 100644 index 97a3914d4..000000000 --- a/bot.svg +++ /dev/null @@ -1,3795 +0,0 @@ - - - - - - -G - - - -bot_cogs_clean - -bot.cogs.clean - - - -requests - -requests - - - -discord_Webhook - -discord. -Webhook - - - -requests->discord_Webhook - - - - - -bot_cogs_doc - -bot.cogs.doc - - - -requests->bot_cogs_doc - - - - - -bot_bot - -bot.bot - - - -bot_bot->bot_cogs_clean - - - - - -bot_cogs_moderation_management - -bot. -cogs. -moderation. -management - - - -bot_bot->bot_cogs_moderation_management - - - - - -bot_cogs_webhook_remover - -bot. -cogs. -webhook_remover - - - -bot_bot->bot_cogs_webhook_remover - - - - - - - -bot_cogs_verification - -bot. -cogs. -verification - - - -bot_bot->bot_cogs_verification - - - - - -bot_cogs_wolfram - -bot. -cogs. -wolfram - - - -bot_bot->bot_cogs_wolfram - - - - - - -bot_cogs_site - -bot.cogs.site - - - -bot_bot->bot_cogs_site - - - - - -bot_cogs_watchchannels_watchchannel - -bot. -cogs. -watchchannels. -watchchannel - - - -bot_bot->bot_cogs_watchchannels_watchchannel - - - - - - -bot_cogs_extensions - -bot. -cogs. -extensions - - - -bot_bot->bot_cogs_extensions - - - - - -bot_cogs_antimalware - -bot. -cogs. -antimalware - - - -bot_bot->bot_cogs_antimalware - - - - - -bot___main__ - -bot.__main__ - - - -bot_bot->bot___main__ - - - - - -bot_cogs_moderation - -bot. -cogs. -moderation - - - -bot_bot->bot_cogs_moderation - - - - - -bot_cogs_duck_pond - -bot. -cogs. -duck_pond - - - -bot_bot->bot_cogs_duck_pond - - - - - - -bot_cogs_antispam - -bot. -cogs. -antispam - - - -bot_bot->bot_cogs_antispam - - - - - -bot_interpreter - -bot. -interpreter - - - -bot_bot->bot_interpreter - - - - - -bot_cogs_moderation_superstarify - -bot. -cogs. -moderation. -superstarify - - - -bot_bot->bot_cogs_moderation_superstarify - - - - -bot_cogs_defcon - -bot. -cogs. -defcon - - - -bot_bot->bot_cogs_defcon - - - - - - -bot_cogs_moderation_ModLog - -bot. -cogs. -moderation. -ModLog - - - -bot_bot->bot_cogs_moderation_ModLog - - - - - -bot_cogs_watchchannels_talentpool - -bot. -cogs. -watchchannels. -talentpool - - - -bot_bot->bot_cogs_watchchannels_talentpool - - - - -bot_cogs_information - -bot. -cogs. -information - - - -bot_bot->bot_cogs_information - - - - - -bot_cogs_off_topic_names - -bot. -cogs. -off_topic_names - - - -bot_bot->bot_cogs_off_topic_names - - - - - - -bot_cogs_alias - -bot.cogs.alias - - - -bot_bot->bot_cogs_alias - - - - - - -bot_cogs_moderation_silence - -bot. -cogs. -moderation. -silence - - - -bot_bot->bot_cogs_moderation_silence - - - - - -bot_cogs_sync_syncers - -bot. -cogs. -sync. -syncers - - - -bot_bot->bot_cogs_sync_syncers - - - - - - -bot_cogs_moderation_scheduler - -bot. -cogs. -moderation. -scheduler - - - -bot_bot->bot_cogs_moderation_scheduler - - - - - - -bot_cogs_eval - -bot.cogs.eval - - - -bot_bot->bot_cogs_eval - - - - - -bot_cogs_help_channels - -bot. -cogs. -help_channels - - - -bot_bot->bot_cogs_help_channels - - - - - -bot_cogs_tags - -bot.cogs.tags - - - -bot_bot->bot_cogs_tags - - - - - - -bot_cogs_reddit - -bot. -cogs. -reddit - - - -bot_bot->bot_cogs_reddit - - - - - - -bot_cogs_utils - -bot.cogs.utils - - - -bot_bot->bot_cogs_utils - - - - - -bot_cogs_sync - -bot.cogs.sync - - - -bot_bot->bot_cogs_sync - - - - - -bot_cogs_help - -bot.cogs.help - - - -bot_bot->bot_cogs_help - - - - - -bot_cogs_bot - -bot.cogs.bot - - - -bot_bot->bot_cogs_bot - - - - - - - - -bot_cogs_watchchannels_bigbrother - -bot. -cogs. -watchchannels. -bigbrother - - - -bot_bot->bot_cogs_watchchannels_bigbrother - - - - -bot_cogs_logging - -bot. -cogs. -logging - - - -bot_bot->bot_cogs_logging - - - - - - -bot_cogs_config_verifier - -bot. -cogs. -config_verifier - - - -bot_bot->bot_cogs_config_verifier - - - - - - -bot_cogs_moderation_modlog - -bot. -cogs. -moderation. -modlog - - - -bot_bot->bot_cogs_moderation_modlog - - - - - - -bot_cogs_sync_cog - -bot. -cogs. -sync. -cog - - - -bot_bot->bot_cogs_sync_cog - - - - - - -bot_cogs_snekbox - -bot. -cogs. -snekbox - - - -bot_bot->bot_cogs_snekbox - - - - - -bot_cogs_moderation_infractions - -bot. -cogs. -moderation. -infractions - - - -bot_bot->bot_cogs_moderation_infractions - - - - - -bot_cogs_watchchannels - -bot. -cogs. -watchchannels - - - -bot_bot->bot_cogs_watchchannels - - - - - -bot_bot->bot_cogs_doc - - - - - -bot_cogs_security - -bot. -cogs. -security - - - -bot_bot->bot_cogs_security - - - - - -bot_cogs_reminders - -bot. -cogs. -reminders - - - -bot_bot->bot_cogs_reminders - - - - - -bot_cogs_token_remover - -bot. -cogs. -token_remover - - - -bot_bot->bot_cogs_token_remover - - - - - - -bot_cogs_filtering - -bot. -cogs. -filtering - - - -bot_bot->bot_cogs_filtering - - - - - -bot_cogs_jams - -bot.cogs.jams - - - -bot_bot->bot_cogs_jams - - - - - -bot_cogs_error_handler - -bot. -cogs. -error_handler - - - -bot_bot->bot_cogs_error_handler - - - - - -bot_rules_attachments - -bot. -rules. -attachments - - - -bot_rules - -bot.rules - - - -bot_rules_attachments->bot_rules - - - - - -bot_cogs_stats - -bot.cogs.stats - - - -bot_api - -bot.api - - - -bot_api->bot_bot - - - - - -bot_api->bot_cogs_watchchannels_watchchannel - - - - -bot_cogs_moderation_utils - -bot. -cogs. -moderation. -utils - - - -bot_api->bot_cogs_moderation_utils - - - - - -bot_api->bot_cogs_watchchannels_talentpool - - - - -bot_api->bot_cogs_off_topic_names - - - - - -bot_api->bot_cogs_sync_syncers - - - - - -bot_api->bot_cogs_moderation_scheduler - - - - -bot_api->bot_cogs_sync_cog - - - - - -bot_api->bot_cogs_error_handler - - - - - - - -bot_cogs_moderation_management->bot_cogs_moderation - - - - - -urllib3_exceptions - -urllib3. -exceptions - - - -urllib3_exceptions->requests - - - - - -urllib3 - -urllib3 - - - -urllib3_exceptions->urllib3 - - - - - -urllib3_exceptions->bot_cogs_doc - - - - - -dateutil_parser - -dateutil. -parser - - - -bot_converters - -bot.converters - - - -dateutil_parser->bot_converters - - - - - -dateutil_parser->bot_cogs_watchchannels_watchchannel - - - - - - -bot_utils_time - -bot.utils.time - - - -dateutil_parser->bot_utils_time - - - - - -dateutil_parser->bot_cogs_moderation_scheduler - - - - - - -dateutil_parser->bot_cogs_reminders - - - - -dateutil_relativedelta - -dateutil. -relativedelta - - - -dateutil_relativedelta->bot_cogs_wolfram - - - - - - -dateutil_relativedelta->bot_converters - - - - - - -dateutil_relativedelta->bot_cogs_moderation_ModLog - - - - - - -dateutil_relativedelta->bot_utils_time - - - - - -dateutil_relativedelta->bot_cogs_utils - - - - - -dateutil_relativedelta->bot_cogs_moderation_modlog - - - - - -dateutil_relativedelta->bot_cogs_reminders - - - - -dateutil_relativedelta->bot_cogs_filtering - - - - - - -bot_rules_chars - -bot. -rules. -chars - - - -bot_rules_chars->bot_rules - - - - - -discord_Guild - -discord.Guild - - - -discord_Guild->bot_cogs_sync_syncers - - - - - -bot_converters->bot_cogs_moderation_management - - - - -bot_converters->bot_cogs_antispam - - - - - -bot_converters->bot_cogs_moderation_superstarify - - - - -bot_converters->bot_cogs_watchchannels_talentpool - - - - - - -bot_converters->bot_cogs_alias - - - - - -bot_converters->bot_cogs_moderation_silence - - - - -bot_converters->bot_cogs_tags - - - - - -bot_converters->bot_cogs_reddit - - - - - -bot_converters->bot_cogs_watchchannels_bigbrother - - - - -bot_converters->bot_cogs_moderation_infractions - - - - - -bot_converters->bot_cogs_doc - - - - - -bot_converters->bot_cogs_reminders - - - - -bot_converters->bot_cogs_error_handler - - - - - -bs4 - -bs4 - - - -bs4->bot_cogs_doc - - - - - -discord_Client - -discord.Client - - - -bot_utils_messages - -bot. -utils. -messages - - - -discord_Client->bot_utils_messages - - - - -bot_rules_burst_shared - -bot. -rules. -burst_shared - - - -bot_rules_burst_shared->bot_rules - - - - - -bot_cogs_watchchannels_watchchannel->bot_cogs_watchchannels_talentpool - - - - - -bot_cogs_watchchannels_watchchannel->bot_cogs_watchchannels_bigbrother - - - - - -aiohttp - -aiohttp - - - -aiohttp->bot_bot - - - - - -aiohttp->bot_api - - - - - - -aiohttp->bot_converters - - - - - -aiohttp->discord_Client - - - - - - -aiohttp->discord_Webhook - - - - - -aiohttp->bot_cogs_reddit - - - - - -bot_cogs_extensions->bot_cogs_alias - - - - - -discord_User - -discord.User - - - -discord_User->bot_cogs_clean - - - - - - -discord_User->bot_cogs_duck_pond - - - - - - - -discord_User->bot_cogs_sync_syncers - - - - - -discord_User->bot_cogs_help - - - - - -discord_User->bot_cogs_sync_cog - - - - - -discord_User->bot_cogs_snekbox - - - - - -bot_rules->bot_cogs_antispam - - - - - -bot_cogs_moderation->bot_cogs_clean - - - - - -bot_cogs_moderation->bot_cogs_webhook_remover - - - - - -bot_cogs_moderation->bot_cogs_verification - - - - - -bot_cogs_moderation->bot_cogs_watchchannels_watchchannel - - - - - -bot_cogs_moderation->bot_cogs_antispam - - - - - -bot_cogs_moderation->bot_cogs_defcon - - - - - -bot_cogs_moderation->bot_cogs_watchchannels_bigbrother - - - - -bot_cogs_moderation->bot_cogs_token_remover - - - - - -bot_cogs_moderation->bot_cogs_filtering - - - - - -discord - -discord - - - -discord->bot_cogs_clean - - - - - -discord->bot_bot - - - - - -discord->bot_rules_attachments - - - - - -discord->bot_cogs_stats - - - - - -discord->bot_cogs_moderation_management - - - - - -discord->bot_cogs_webhook_remover - - - - -discord->bot_cogs_verification - - - - - - -discord->bot_cogs_wolfram - - - - - - - -discord->bot_rules_chars - - - - - -discord->bot_converters - - - - - -discord->bot_cogs_site - - - - - - -discord->bot_rules_burst_shared - - - - - -discord->bot_cogs_watchchannels_watchchannel - - - - - -discord->bot_cogs_extensions - - - - - -discord->bot_cogs_antimalware - - - - - -discord->bot___main__ - - - - - - -discord->bot_cogs_duck_pond - - - - - -discord->bot_cogs_antispam - - - - -discord->bot_interpreter - - - - - -discord->bot_cogs_moderation_superstarify - - - - - -discord->bot_cogs_defcon - - - - - - -bot_pagination - -bot.pagination - - - -discord->bot_pagination - - - - - -discord->bot_cogs_moderation_ModLog - - - - - - -discord->bot_cogs_moderation_utils - - - - - - -bot_rules_newlines - -bot. -rules. -newlines - - - -discord->bot_rules_newlines - - - - - - -discord->bot_cogs_watchchannels_talentpool - - - - - -discord->bot_cogs_information - - - - - -discord->bot_cogs_off_topic_names - - - - - -discord->bot_cogs_alias - - - - - -discord->bot_cogs_moderation_silence - - - - - - -discord->bot_cogs_sync_syncers - - - - - - -discord->bot_cogs_moderation_scheduler - - - - - - -discord->bot_cogs_eval - - - - - - -bot_rules_burst - -bot. -rules. -burst - - - -discord->bot_rules_burst - - - - - -discord->bot_cogs_help_channels - - - - - - -discord->bot_cogs_tags - - - - - -discord->bot_cogs_reddit - - - - - - -discord->bot_cogs_utils - - - - - -bot_rules_discord_emojis - -bot. -rules. -discord_emojis - - - -discord->bot_rules_discord_emojis - - - - - -bot_rules_duplicates - -bot. -rules. -duplicates - - - -discord->bot_rules_duplicates - - - - - -discord->bot_cogs_help - - - - - -discord->bot_cogs_bot - - - - - - - -discord->bot_cogs_logging - - - - - - -bot_rules_mentions - -bot. -rules. -mentions - - - -discord->bot_rules_mentions - - - - - -bot_decorators - -bot.decorators - - - -discord->bot_decorators - - - - - -discord->bot_cogs_moderation_modlog - - - - - -discord->bot_cogs_sync_cog - - - - - -discord->bot_cogs_snekbox - - - - - -discord->bot_cogs_moderation_infractions - - - - - - -bot_rules_links - -bot. -rules. -links - - - -discord->bot_rules_links - - - - - - -discord->bot_cogs_doc - - - - - -discord->bot_utils_messages - - - - - -bot_patches_message_edited_at - -bot. -patches. -message_edited_at - - - -discord->bot_patches_message_edited_at - - - - - - - -discord->bot_cogs_reminders - - - - - -bot_rules_role_mentions - -bot. -rules. -role_mentions - - - -discord->bot_rules_role_mentions - - - - - -discord->bot_cogs_token_remover - - - - - -discord->bot_cogs_filtering - - - - - -discord->bot_cogs_jams - - - - - -bot_constants - -bot.constants - - - -bot_constants->bot_cogs_clean - - - - - -bot_constants->bot_bot - - - - - -bot_constants->bot_api - - - - - -bot_constants->bot_cogs_moderation_management - - - - - -bot_constants->bot_cogs_webhook_remover - - - - -bot_constants->bot_cogs_verification - - - - - -bot_constants->bot_cogs_wolfram - - - - -bot_constants->bot_cogs_site - - - - - - -bot_constants->bot_cogs_watchchannels_watchchannel - - - - - -bot_constants->bot_cogs_extensions - - - - - -bot_constants->bot_cogs_antimalware - - - - - -bot_constants->bot___main__ - - - - - -bot_constants->bot_cogs_duck_pond - - - - - - -bot_constants->bot_cogs_antispam - - - - - -bot_constants->bot_cogs_moderation_superstarify - - - - -bot_constants->bot_cogs_defcon - - - - - -bot_constants->bot_pagination - - - - - - -bot_constants->bot_cogs_moderation_ModLog - - - - - - -bot_constants->bot_cogs_moderation_utils - - - - - -bot_constants->bot_cogs_watchchannels_talentpool - - - - - - -bot_constants->bot_cogs_information - - - - - - -bot_constants->bot_cogs_off_topic_names - - - - - -bot_constants->bot_cogs_moderation_silence - - - - - - -bot_constants->bot_cogs_sync_syncers - - - - - -bot_constants->bot_cogs_moderation_scheduler - - - - - - -bot_constants->bot_cogs_eval - - - - - -bot_constants->bot_cogs_help_channels - - - - - -bot_constants->bot_cogs_tags - - - - - - -bot_constants->bot_cogs_reddit - - - - -bot_constants->bot_cogs_utils - - - - - -bot_constants->bot_cogs_help - - - - - - - -bot_constants->bot_cogs_bot - - - - - -bot_constants->bot_cogs_watchchannels_bigbrother - - - - - - -bot_constants->bot_cogs_logging - - - - - - -bot_constants->bot_cogs_config_verifier - - - - - -bot_constants->bot_decorators - - - - -bot_constants->bot_cogs_moderation_modlog - - - - - - -bot_constants->bot_cogs_sync_cog - - - - - - -bot_constants->bot_cogs_snekbox - - - - - - -bot_constants->bot_cogs_moderation_infractions - - - - - - -bot_constants->bot_cogs_doc - - - - - -bot_constants->bot_utils_messages - - - - - - -bot_constants->bot_cogs_reminders - - - - -bot_constants->bot_cogs_token_remover - - - - - -bot_constants->bot_cogs_filtering - - - - - - - - -bot_constants->bot_cogs_jams - - - - - -bot_constants->bot_cogs_error_handler - - - - -discord_File - -discord.File - - - -discord_File->bot_utils_messages - - - - -discord_Reaction - -discord. -Reaction - - - -discord_Reaction->bot_cogs_sync_syncers - - - - - -discord_Reaction->bot_cogs_help - - - - - -discord_Reaction->bot_cogs_snekbox - - - - - -discord_Reaction->bot_utils_messages - - - - - -bot_interpreter->bot_cogs_eval - - - - - - -bot_cogs_moderation_superstarify->bot_cogs_moderation - - - - - - -more_itertools - -more_itertools - - - -more_itertools->bot_cogs_jams - - - - - -statsd - -statsd - - - -statsd->bot_bot - - - - - -bot_pagination->bot_cogs_moderation_management - - - - - - - -bot_pagination->bot_cogs_wolfram - - - - - -bot_pagination->bot_cogs_site - - - - - -bot_pagination->bot_cogs_watchchannels_watchchannel - - - - - -bot_pagination->bot_cogs_extensions - - - - - -bot_pagination->bot_cogs_watchchannels_talentpool - - - - - -bot_pagination->bot_cogs_information - - - - - -bot_pagination->bot_cogs_off_topic_names - - - - - - -bot_pagination->bot_cogs_alias - - - - - -bot_pagination->bot_cogs_tags - - - - - -bot_pagination->bot_cogs_reddit - - - - - - -bot_pagination->bot_cogs_help - - - - - -bot_pagination->bot_cogs_doc - - - - - -bot_pagination->bot_cogs_reminders - - - - - -bot_utils_checks - -bot. -utils. -checks - - - -bot_utils_checks->bot_cogs_moderation_management - - - - - -bot_utils_checks->bot_cogs_verification - - - - - -bot_utils_checks->bot_cogs_extensions - - - - - - -bot_utils_checks->bot_cogs_moderation_superstarify - - - - - -bot_utils_checks->bot_cogs_information - - - - - -bot_utils_checks->bot_cogs_moderation_silence - - - - - - -bot_utils_checks->bot_decorators - - - - - -bot_utils_checks->bot_cogs_moderation_infractions - - - - - - -bot_utils_checks->bot_cogs_reminders - - - - -bot_cogs_moderation_ModLog->bot_cogs_clean - - - - - - -bot_cogs_moderation_ModLog->bot_cogs_verification - - - - - -bot_cogs_moderation_ModLog->bot_cogs_watchchannels_watchchannel - - - - - -bot_cogs_moderation_ModLog->bot_cogs_antispam - - - - - -bot_cogs_moderation_ModLog->bot_cogs_defcon - - - - - - -bot_cogs_moderation_ModLog->bot_cogs_token_remover - - - - - - - -bot_cogs_moderation_ModLog->bot_cogs_filtering - - - - - - - -discord_Object - -discord.Object - - - -discord_Object->bot_cogs_verification - - - - - -discord_Object->bot_cogs_antispam - - - - - - -bot_cogs_moderation_utils->bot_cogs_moderation_management - - - - - -bot_cogs_moderation_utils->bot_cogs_moderation_superstarify - - - - - - -bot_cogs_moderation_utils->bot_cogs_moderation_scheduler - - - - - -bot_cogs_moderation_utils->bot_cogs_watchchannels_bigbrother - - - - - - - -bot_cogs_moderation_utils->bot_cogs_moderation_infractions - - - - - -discord_Webhook->bot_utils_messages - - - - - - - -bot_rules_newlines->bot_rules - - - - - -bot_cogs_watchchannels_talentpool->bot_cogs_watchchannels - - - - - -discord_utils - -discord.utils - - - -discord_utils->discord_Guild - - - - - -discord_utils->discord_Client - - - - -discord_utils->discord_User - - - - - -discord_utils->discord - - - - - -discord_utils->bot_cogs_moderation_ModLog - - - - - - -discord_utils->discord_Object - - - - - -discord_utils->discord_Webhook - - - - - -discord_utils->bot_cogs_information - - - - - - -discord_Message - -discord. -Message - - - -discord_utils->discord_Message - - - - - -discord_abc - -discord.abc - - - -discord_utils->discord_abc - - - - - -discord_message - -discord. -message - - - -discord_utils->discord_message - - - - - -discord_Role - -discord.Role - - - -discord_utils->discord_Role - - - - - -discord_Member - -discord.Member - - - -discord_utils->discord_Member - - - - - - -discord_utils->bot_cogs_moderation_modlog - - - - - -discord_utils->bot_patches_message_edited_at - - - - - -discord_utils->bot_cogs_token_remover - - - - - -discord_utils->bot_cogs_filtering - - - - - - -discord_utils->bot_cogs_jams - - - - - -bot_cogs_moderation_silence->bot_cogs_moderation - - - - - -bot_utils_time->bot_cogs_moderation_management - - - - - - -bot_utils_time->bot_cogs_wolfram - - - - - -bot_utils_time->bot_cogs_watchchannels_watchchannel - - - - - -bot_utils_time->bot_cogs_moderation_superstarify - - - - - -bot_utils_time->bot_cogs_moderation_ModLog - - - - - -bot_utils_time->bot_cogs_watchchannels_talentpool - - - - -bot_utils_time->bot_cogs_information - - - - - - -bot_utils_time->bot_cogs_moderation_scheduler - - - - - -bot_utils_time->bot_cogs_utils - - - - - -bot_utils_time->bot_cogs_moderation_modlog - - - - - - - -bot_utils_time->bot_cogs_reminders - - - - - -discord_Message->bot_cogs_clean - - - - - -discord_Message->bot_rules_attachments - - - - - -discord_Message->bot_cogs_stats - - - - - -discord_Message->bot_cogs_webhook_remover - - - - - - - -discord_Message->bot_cogs_verification - - - - - -discord_Message->bot_rules_chars - - - - - - -discord_Message->bot_rules_burst_shared - - - - - -discord_Message->bot_cogs_watchchannels_watchchannel - - - - - - -discord_Message->bot_cogs_antimalware - - - - - -discord_Message->bot_cogs_duck_pond - - - - - -discord_Message->bot_cogs_antispam - - - - - -discord_Message->bot_rules_newlines - - - - - - -discord_Message->bot_cogs_information - - - - - -discord_Message->bot_cogs_sync_syncers - - - - - -discord_Message->bot_rules_burst - - - - - - - -discord_Message->bot_cogs_utils - - - - - -discord_Message->bot_rules_discord_emojis - - - - - - -discord_Message->bot_rules_duplicates - - - - - - -discord_Message->bot_cogs_help - - - - - - - - -discord_Message->bot_cogs_bot - - - - - - -discord_Message->bot_rules_mentions - - - - - - -discord_Message->bot_cogs_snekbox - - - - - - - -discord_Message->bot_rules_links - - - - - - -discord_Message->bot_utils_messages - - - - - -discord_Message->bot_rules_role_mentions - - - - - -discord_Message->bot_cogs_token_remover - - - - - - -discord_Message->bot_cogs_filtering - - - - - - - -bot_cogs_sync_syncers->bot_cogs_sync_cog - - - - - -dateutil - -dateutil - - - -dateutil->bot_cogs_wolfram - - - - - -dateutil->bot_converters - - - - - - - -dateutil->bot_cogs_watchchannels_watchchannel - - - - -dateutil->bot_cogs_moderation_ModLog - - - - - -dateutil->bot_utils_time - - - - - -dateutil->bot_cogs_moderation_scheduler - - - - - -dateutil->bot_cogs_utils - - - - - -dateutil->bot_cogs_moderation_modlog - - - - - -dateutil->bot_cogs_reminders - - - - -dateutil->bot_cogs_filtering - - - - - -bot_cogs_moderation_scheduler->bot_cogs_moderation_superstarify - - - - - -bot_cogs_moderation_scheduler->bot_cogs_moderation_infractions - - - - - -bot_rules_burst->bot_rules - - - - - -discord_abc->discord_User - - - - - -discord_abc->discord - - - - - -discord_abc->bot_pagination - - - - - -discord_abc->bot_cogs_moderation_ModLog - - - - - -discord_abc->discord_Member - - - - - -discord_abc->bot_cogs_moderation_modlog - - - - - -discord_abc->bot_utils_messages - - - - -discord_message->discord - - - - - -discord_message->discord_Webhook - - - - - -discord_message->bot_patches_message_edited_at - - - - - - -bot_rules_discord_emojis->bot_rules - - - - - -yaml - -yaml - - - -yaml->bot_constants - - - - - -bot_rules_duplicates->bot_rules - - - - - -urllib3->requests - - - - - -urllib3->bot_cogs_doc - - - - - - -discord_Role->bot_cogs_information - - - - - -discord_Role->bot_cogs_utils - - - - - -discord_Role->bot_cogs_sync_cog - - - - - -discord_Colour - -discord.Colour - - - -discord_Colour->bot_cogs_clean - - - - -discord_Colour->bot_cogs_webhook_remover - - - - - -discord_Colour->bot_cogs_verification - - - - - -discord_Colour->bot_cogs_site - - - - - - -discord_Colour->bot_cogs_extensions - - - - - -discord_Colour->bot_cogs_antispam - - - - - -discord_Colour->bot_cogs_moderation_superstarify - - - - - - -discord_Colour->bot_cogs_defcon - - - - - - -discord_Colour->bot_cogs_moderation_ModLog - - - - - - -discord_Colour->bot_cogs_information - - - - - -discord_Colour->bot_cogs_off_topic_names - - - - - -discord_Colour->bot_cogs_alias - - - - - -discord_Colour->bot_cogs_tags - - - - - -discord_Colour->bot_cogs_reddit - - - - - -discord_Colour->bot_cogs_utils - - - - - -discord_Colour->bot_cogs_help - - - - - -discord_Colour->bot_decorators - - - - - -discord_Colour->bot_cogs_moderation_modlog - - - - - - -discord_Colour->bot_cogs_token_remover - - - - - - -discord_Colour->bot_cogs_filtering - - - - - -bot_cogs_watchchannels_bigbrother->bot_cogs_watchchannels - - - - - -discord_Member->bot_rules_attachments - - - - - - - -discord_Member->bot_cogs_stats - - - - - -discord_Member->bot_rules_chars - - - - - -discord_Member->bot_rules_burst_shared - - - - - -discord_Member->bot_cogs_duck_pond - - - - - - -discord_Member->bot_cogs_antispam - - - - -discord_Member->bot_cogs_moderation_superstarify - - - - - -discord_Member->bot_cogs_defcon - - - - - -discord_Member->bot_rules_newlines - - - - - -discord_Member->bot_cogs_watchchannels_talentpool - - - - - - -discord_Member->bot_cogs_information - - - - - -discord_Member->bot_cogs_sync_syncers - - - - - -discord_Member->bot_rules_burst - - - - - -discord_Member->bot_rules_discord_emojis - - - - - -discord_Member->bot_rules_duplicates - - - - -discord_Member->bot_rules_mentions - - - - -discord_Member->bot_decorators - - - - - -discord_Member->bot_cogs_sync_cog - - - - - -discord_Member->bot_cogs_moderation_infractions - - - - - -discord_Member->bot_rules_links - - - - - -discord_Member->bot_utils_messages - - - - - - -discord_Member->bot_rules_role_mentions - - - - - -discord_Member->bot_cogs_filtering - - - - - - - -discord_Member->bot_cogs_jams - - - - - -bot_rules_mentions->bot_rules - - - - - -bot_decorators->bot_cogs_clean - - - - - - - - -bot_decorators->bot_cogs_verification - - - - -bot_decorators->bot_cogs_defcon - - - - - -bot_decorators->bot_cogs_watchchannels_talentpool - - - - - -bot_decorators->bot_cogs_information - - - - - -bot_decorators->bot_cogs_off_topic_names - - - - - -bot_decorators->bot_cogs_eval - - - - - -bot_decorators->bot_cogs_help_channels - - - - - - -bot_decorators->bot_cogs_reddit - - - - - -bot_decorators->bot_cogs_utils - - - - - -bot_decorators->bot_cogs_help - - - - - -bot_decorators->bot_cogs_bot - - - - -bot_decorators->bot_cogs_watchchannels_bigbrother - - - - - -bot_decorators->bot_cogs_snekbox - - - - - - -bot_decorators->bot_cogs_moderation_infractions - - - - - -bot_decorators->bot_cogs_doc - - - - - -bot_decorators->bot_cogs_jams - - - - - -bot_decorators->bot_cogs_error_handler - - - - - -bot_cogs_moderation_modlog->bot_cogs_moderation_management - - - - - - -bot_cogs_moderation_modlog->bot_cogs_webhook_remover - - - - - -bot_cogs_moderation_modlog->bot_cogs_moderation - - - - -bot_cogs_moderation_modlog->bot_cogs_moderation_scheduler - - - - - -bot_patches - -bot.patches - - - -bot_patches->bot___main__ - - - - - -bot_cogs_sync_cog->bot_cogs_sync - - - - - -bot_utils - -bot.utils - - - -bot_utils->bot_cogs_moderation_management - - - - -bot_utils->bot_cogs_verification - - - - - - -bot_utils->bot_cogs_wolfram - - - - - -bot_utils->bot_cogs_watchchannels_watchchannel - - - - - - -bot_utils->bot_cogs_extensions - - - - - -bot_utils->bot_cogs_duck_pond - - - - -bot_utils->bot_cogs_antispam - - - - - - -bot_utils->bot_cogs_moderation_superstarify - - - - - -bot_utils->bot_cogs_moderation_ModLog - - - - - - -bot_utils->bot_cogs_watchchannels_talentpool - - - - - - -bot_utils->bot_cogs_information - - - - -bot_utils->bot_cogs_moderation_silence - - - - - - - -bot_utils->bot_cogs_moderation_scheduler - - - - - -bot_utils->bot_cogs_help_channels - - - - - -bot_utils->bot_cogs_tags - - - - - -bot_utils->bot_cogs_utils - - - - - -bot_utils->bot_cogs_bot - - - - - - -bot_utils->bot_decorators - - - - - -bot_utils->bot_cogs_moderation_modlog - - - - - -bot_utils->bot_cogs_snekbox - - - - - -bot_utils->bot_cogs_moderation_infractions - - - - - - - -bot_utils->bot_cogs_reminders - - - - - - -bot_cogs_moderation_infractions->bot_cogs_moderation_management - - - - - -bot_cogs_moderation_infractions->bot_cogs_moderation - - - - - -bot_rules_links->bot_rules - - - - - -discord_errors - -discord.errors - - - -discord_errors->discord_Guild - - - - - -discord_errors->discord_Client - - - - - - -discord_errors->bot_cogs_watchchannels_watchchannel - - - - -discord_errors->discord_User - - - - - - -discord_errors->discord - - - - - -discord_errors->bot_cogs_duck_pond - - - - - -discord_errors->discord_Webhook - - - - - -discord_errors->discord_utils - - - - - -discord_errors->discord_Message - - - - - - -discord_errors->discord_abc - - - - - -discord_errors->discord_message - - - - - -discord_errors->discord_Role - - - - - - -discord_errors->bot_decorators - - - - - - -discord_errors->bot_cogs_doc - - - - - - -discord_errors->bot_utils_messages - - - - - -discord_errors->bot_cogs_filtering - - - - - -bot_utils_scheduling - -bot. -utils. -scheduling - - - -bot_utils_scheduling->bot_cogs_moderation_scheduler - - - - -bot_utils_scheduling->bot_cogs_help_channels - - - - - -bot_utils_scheduling->bot_cogs_reminders - - - - - -bs4_element - -bs4.element - - - -bs4_element->bs4 - - - - - -bs4_element->bot_cogs_doc - - - - - -dateutil_tz - -dateutil.tz - - - -dateutil_tz->bot_converters - - - - -bot_utils_messages->bot_cogs_watchchannels_watchchannel - - - - - - - -bot_utils_messages->bot_cogs_duck_pond - - - - - -bot_utils_messages->bot_cogs_antispam - - - - - - -bot_utils_messages->bot_cogs_tags - - - - - -bot_utils_messages->bot_cogs_bot - - - - -bot_utils_messages->bot_cogs_snekbox - - - - -bot_patches_message_edited_at->bot_patches - - - - - -bot_rules_role_mentions->bot_rules - - - - - -bot_cogs_token_remover->bot_cogs_bot - - - - - diff --git a/bot/cogs/stats.py b/bot/cogs/stats.py index e963dc312..8fb7d8639 100644 --- a/bot/cogs/stats.py +++ b/bot/cogs/stats.py @@ -1,14 +1,14 @@ from discord import Member, Message, Status from discord.ext.commands import Bot, Cog, Context -from bot.constants import Guild +from bot.constants import Channels, Guild CHANNEL_NAME_OVERRIDES = { - Guild.channels.off_topic_0: "off_topic_0", - Guild.channels.off_topic_1: "off_topic_1", - Guild.channels.off_topic_2: "off_topic_2", - Guild.channels.staff_lounge: "staff_lounge" + Channels.off_topic_0: "off_topic_0", + Channels.off_topic_1: "off_topic_1", + Channels.off_topic_2: "off_topic_2", + Channels.staff_lounge: "staff_lounge" } -- cgit v1.2.3 From bca47a1c2e59fb112b947876cea1836879ac7282 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 11 Apr 2020 21:02:13 +0100 Subject: Implement an AsyncStatsClient to send statsd communications asynchronously --- bot/async_stats.py | 39 +++++++++++++++++++++++++++++++++++++++ bot/bot.py | 13 ++++++++++--- 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 bot/async_stats.py diff --git a/bot/async_stats.py b/bot/async_stats.py new file mode 100644 index 000000000..58a80f528 --- /dev/null +++ b/bot/async_stats.py @@ -0,0 +1,39 @@ +import asyncio +import socket + +from statsd.client.base import StatsClientBase + + +class AsyncStatsClient(StatsClientBase): + """An async transport method for statsd communication.""" + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + host: str = 'localhost', + port: int = 8125, + prefix: str = None + ): + """Create a new client.""" + family, _, _, _, addr = socket.getaddrinfo( + host, port, socket.AF_INET, socket.SOCK_DGRAM)[0] + self._addr = addr + self._prefix = prefix + self._loop = loop + self._transport = None + + async def create_socket(self) -> None: + """Use the loop.create_datagram_endpoint method to create a socket.""" + self._transport, _ = await self._loop.create_datagram_endpoint( + asyncio.DatagramProtocol, + family=socket.AF_INET, + remote_addr=self._addr + ) + + def _send(self, data: str) -> None: + """Start an async task to send data to statsd.""" + self._loop.create_task(self._async_send(data)) + + async def _async_send(self, data: str) -> None: + """Send data to the statsd server using the async transport.""" + self._transport.sendto(data.encode('ascii'), self._addr) diff --git a/bot/bot.py b/bot/bot.py index 65081e438..c5d490409 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -6,10 +6,10 @@ from typing import Optional import aiohttp import discord -import statsd from discord.ext import commands from bot import DEBUG_MODE, api, constants +from bot.async_stats import AsyncStatsClient log = logging.getLogger('bot') @@ -41,7 +41,7 @@ class Bot(commands.Bot): # will effectively disable stats. statsd_url = "127.0.0.1" - self.stats = statsd.StatsClient(statsd_url, 8125, prefix="bot") + self.stats = AsyncStatsClient(self.loop, statsd_url, 8125, prefix="bot") def add_cog(self, cog: commands.Cog) -> None: """Adds a "cog" to the bot and logs the operation.""" @@ -60,7 +60,7 @@ class Bot(commands.Bot): super().clear() async def close(self) -> None: - """Close the Discord connection and the aiohttp session, connector, and resolver.""" + """Close the Discord connection and the aiohttp session, connector, statsd client, and resolver.""" await super().close() await self.api_client.close() @@ -74,6 +74,9 @@ class Bot(commands.Bot): if self._resolver: await self._resolver.close() + if self.stats._transport: + await self.stats._transport.close() + async def login(self, *args, **kwargs) -> None: """Re-create the connector and set up sessions before logging into Discord.""" self._recreate() @@ -111,6 +114,10 @@ class Bot(commands.Bot): self.http_session = aiohttp.ClientSession(connector=self._connector) self.api_client.recreate(force=True, connector=self._connector) + async def on_ready(self) -> None: + """Construct an asynchronous transport for the statsd client.""" + await self.stats.create_socket() + async def on_guild_available(self, guild: discord.Guild) -> None: """ Set the internal guild available event when constants.Guild.id becomes available. -- cgit v1.2.3 From 7da559647db7dfd4386f1711e2c053efd9a6c897 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 11 Apr 2020 21:37:49 +0100 Subject: Move create_socket to the login method of the bot --- bot/bot.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index c5d490409..ef4a325dc 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -80,6 +80,7 @@ class Bot(commands.Bot): async def login(self, *args, **kwargs) -> None: """Re-create the connector and set up sessions before logging into Discord.""" self._recreate() + await self.stats.create_socket() await super().login(*args, **kwargs) def _recreate(self) -> None: @@ -114,10 +115,6 @@ class Bot(commands.Bot): self.http_session = aiohttp.ClientSession(connector=self._connector) self.api_client.recreate(force=True, connector=self._connector) - async def on_ready(self) -> None: - """Construct an asynchronous transport for the statsd client.""" - await self.stats.create_socket() - async def on_guild_available(self, guild: discord.Guild) -> None: """ Set the internal guild available event when constants.Guild.id becomes available. -- cgit v1.2.3 From ee5a4df9537b46cdceb35243d887e84601d07795 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 11 Apr 2020 22:11:43 +0100 Subject: Additional statistics --- bot/cogs/defcon.py | 2 ++ bot/cogs/error_handler.py | 14 +++++++++++++- bot/cogs/help_channels.py | 24 ++++++++++++++++++++---- bot/cogs/stats.py | 14 ++++++++++---- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 06b2f25c6..9197dcca3 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -146,6 +146,8 @@ class Defcon(Cog): await ctx.send(self.build_defcon_msg(action, error)) await self.send_defcon_log(action, ctx.author, error) + self.bot.stats.gauge("defcon.days", days) + @defcon_group.command(name='enable', aliases=('on', 'e')) @with_role(Roles.admins, Roles.owners) async def enable_command(self, ctx: Context) -> None: diff --git a/bot/cogs/error_handler.py b/bot/cogs/error_handler.py index 722376cc6..dae283c6a 100644 --- a/bot/cogs/error_handler.py +++ b/bot/cogs/error_handler.py @@ -171,19 +171,25 @@ class ErrorHandler(Cog): if isinstance(e, errors.MissingRequiredArgument): await ctx.send(f"Missing required argument `{e.param.name}`.") await ctx.invoke(*help_command) + self.bot.stats.incr("errors.missing_required_argument") elif isinstance(e, errors.TooManyArguments): await ctx.send(f"Too many arguments provided.") await ctx.invoke(*help_command) + self.bot.stats.incr("errors.too_many_arguments") elif isinstance(e, errors.BadArgument): await ctx.send(f"Bad argument: {e}\n") await ctx.invoke(*help_command) + self.bot.stats.incr("errors.bad_argument") elif isinstance(e, errors.BadUnionArgument): await ctx.send(f"Bad argument: {e}\n```{e.errors[-1]}```") + self.bot.stats.incr("errors.bad_union_argument") elif isinstance(e, errors.ArgumentParsingError): await ctx.send(f"Argument parsing error: {e}") + self.bot.stats.incr("errors.argument_parsing_error") else: await ctx.send("Something about your input seems off. Check the arguments:") await ctx.invoke(*help_command) + self.bot.stats.incr("errors.other_user_input_error") @staticmethod async def handle_check_failure(ctx: Context, e: errors.CheckFailure) -> None: @@ -205,10 +211,12 @@ class ErrorHandler(Cog): ) if isinstance(e, bot_missing_errors): + ctx.bot.stats.incr("errors.bot_permission_error") await ctx.send( f"Sorry, it looks like I don't have the permissions or roles I need to do that." ) elif isinstance(e, (InChannelCheckFailure, errors.NoPrivateMessage)): + ctx.bot.stats.incr("errors.wrong_channel_or_dm_error") await ctx.send(e) @staticmethod @@ -217,16 +225,20 @@ class ErrorHandler(Cog): if e.status == 404: await ctx.send("There does not seem to be anything matching your query.") log.debug(f"API responded with 404 for command {ctx.command}") + ctx.bot.stats.incr("errors.api_error_404") elif e.status == 400: content = await e.response.json() log.debug(f"API responded with 400 for command {ctx.command}: %r.", content) await ctx.send("According to the API, your request is malformed.") + ctx.bot.stats.incr("errors.api_error_400") elif 500 <= e.status < 600: await ctx.send("Sorry, there seems to be an internal issue with the API.") log.warning(f"API responded with {e.status} for command {ctx.command}") + ctx.bot.stats.incr("errors.api_internal_server_error") else: await ctx.send(f"Got an unexpected status code from the API (`{e.status}`).") log.warning(f"Unexpected API response for command {ctx.command}: {e.status}") + ctx.bot.stats.incr(f"errors.api_error_{e.status}") @staticmethod async def handle_unexpected_error(ctx: Context, e: errors.CommandError) -> None: @@ -236,7 +248,7 @@ class ErrorHandler(Cog): f"```{e.__class__.__name__}: {e}```" ) - ctx.bot.stats.incr("errors.commands") + ctx.bot.stats.incr("errors.unexpected") with push_scope() as scope: scope.user = { diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 389a4ad2a..01a77db2b 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -127,6 +127,9 @@ class HelpChannels(Scheduler, commands.Cog): self.on_message_lock = asyncio.Lock() self.init_task = self.bot.loop.create_task(self.init_cog()) + # Stats + self.claim_times = {} + def cog_unload(self) -> None: """Cancel the init task and scheduled tasks when the cog unloads.""" log.trace("Cog unload: cancelling the init_cog task") @@ -195,7 +198,7 @@ class HelpChannels(Scheduler, commands.Cog): if ctx.channel.category == self.in_use_category: self.cancel_task(ctx.channel.id) - await self.move_to_dormant(ctx.channel) + await self.move_to_dormant(ctx.channel, "command") else: log.debug(f"{ctx.author} invoked command 'dormant' outside an in-use help channel") @@ -406,7 +409,7 @@ class HelpChannels(Scheduler, commands.Cog): f"and will be made dormant." ) - await self.move_to_dormant(channel) + await self.move_to_dormant(channel, "auto") else: # Cancel the existing task, if any. if has_task: @@ -446,8 +449,12 @@ class HelpChannels(Scheduler, commands.Cog): await self.ensure_permissions_synchronization(self.available_category) self.report_stats() - async def move_to_dormant(self, channel: discord.TextChannel) -> None: - """Make the `channel` dormant.""" + async def move_to_dormant(self, channel: discord.TextChannel, caller: str) -> None: + """ + Make the `channel` dormant. + + A caller argument is provided for metrics. + """ log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") await channel.edit( @@ -457,6 +464,13 @@ class HelpChannels(Scheduler, commands.Cog): topic=DORMANT_TOPIC, ) + self.bot.stats.incr(f"help.dormant_calls.{caller}") + + if self.claim_times.get(channel.id): + claimed = self.claim_times[channel.id] + in_use_time = datetime.now() - claimed + self.bot.stats.timer("help.in_use_time", in_use_time) + log.trace(f"Position of #{channel} ({channel.id}) is actually {channel.position}.") log.trace(f"Sending dormant message for #{channel} ({channel.id}).") @@ -560,6 +574,8 @@ class HelpChannels(Scheduler, commands.Cog): self.bot.stats.incr("help.claimed") + self.claim_times[channel.id] = datetime.now() + log.trace(f"Releasing on_message lock for {message.id}.") # Move a dormant channel to the Available category to fill in the gap. diff --git a/bot/cogs/stats.py b/bot/cogs/stats.py index 8fb7d8639..772ae2c97 100644 --- a/bot/cogs/stats.py +++ b/bot/cogs/stats.py @@ -1,3 +1,5 @@ +import string + from discord import Member, Message, Status from discord.ext.commands import Bot, Cog, Context @@ -11,6 +13,8 @@ CHANNEL_NAME_OVERRIDES = { Channels.staff_lounge: "staff_lounge" } +ALLOWED_CHARS = string.ascii_letters + string.digits + class Stats(Cog): """A cog which provides a way to hook onto Discord events and forward to stats.""" @@ -32,6 +36,8 @@ class Stats(Cog): if CHANNEL_NAME_OVERRIDES.get(message.channel.id): reformatted_name = CHANNEL_NAME_OVERRIDES.get(message.channel.id) + reformatted_name = "".join([char for char in reformatted_name if char in ALLOWED_CHARS]) + stat_name = f"channels.{reformatted_name}" self.bot.stats.incr(stat_name) @@ -73,13 +79,13 @@ class Stats(Cog): offline = 0 for member in after.guild.members: - if member.status == Status.online: + if member.status is Status.online: online += 1 - elif member.status == Status.dnd: + elif member.status is Status.dnd: dnd += 1 - elif member.status == Status.idle: + elif member.status is Status.idle: idle += 1 - else: + elif member.status is Status.offline: offline += 1 self.bot.stats.gauge("guild.status.online", online) -- cgit v1.2.3 From ea9db39715199ea05eafe124dbf74231d1e7e3d4 Mon Sep 17 00:00:00 2001 From: Joseph Date: Sat, 11 Apr 2020 22:25:44 +0100 Subject: Update bot/cogs/stats.py Co-Authored-By: Mark --- bot/cogs/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/stats.py b/bot/cogs/stats.py index 772ae2c97..c15d0eb1b 100644 --- a/bot/cogs/stats.py +++ b/bot/cogs/stats.py @@ -36,7 +36,7 @@ class Stats(Cog): if CHANNEL_NAME_OVERRIDES.get(message.channel.id): reformatted_name = CHANNEL_NAME_OVERRIDES.get(message.channel.id) - reformatted_name = "".join([char for char in reformatted_name if char in ALLOWED_CHARS]) + reformatted_name = "".join(char for char in reformatted_name if char in ALLOWED_CHARS) stat_name = f"channels.{reformatted_name}" self.bot.stats.incr(stat_name) -- cgit v1.2.3 From 33c40ce2913e2a324647c4eb8f5c511cb26cf8ae Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 11 Apr 2020 22:28:24 +0100 Subject: Use in for membership check as opposed to .get() --- bot/cogs/help_channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 01a77db2b..632c78701 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -466,7 +466,7 @@ class HelpChannels(Scheduler, commands.Cog): self.bot.stats.incr(f"help.dormant_calls.{caller}") - if self.claim_times.get(channel.id): + if channel.id in self.claim_times: claimed = self.claim_times[channel.id] in_use_time = datetime.now() - claimed self.bot.stats.timer("help.in_use_time", in_use_time) -- cgit v1.2.3 From 673869062ff49a74a2d2f8338c4d26003bee995e Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sat, 11 Apr 2020 22:44:13 +0100 Subject: Add a metric for tracking how long defcon was active --- bot/cogs/defcon.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 9197dcca3..7043c7cbb 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -126,6 +126,19 @@ class Defcon(Cog): 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.timer("defcon.enabled", delta) + except Exception: + pass + error = None try: await self.bot.api_client.put( @@ -136,6 +149,7 @@ class Defcon(Cog): # TODO: retrieve old days count 'days': days, 'enabled': action is not Action.DISABLED, + 'enable_date': datetime.now().isoformat() } } ) @@ -146,7 +160,7 @@ class Defcon(Cog): await ctx.send(self.build_defcon_msg(action, error)) await self.send_defcon_log(action, ctx.author, error) - self.bot.stats.gauge("defcon.days", days) + self.bot.stats.gauge("defcon.threshold", days) @defcon_group.command(name='enable', aliases=('on', 'e')) @with_role(Roles.admins, Roles.owners) -- cgit v1.2.3 From 4adcb0b0ab2b8768e31043429bdfcbf4dab607e6 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sun, 12 Apr 2020 00:09:53 +0100 Subject: Address aeros' review comment regarding help channel stat reporting --- bot/cogs/help_channels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 632c78701..d260a6a33 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -374,9 +374,9 @@ class HelpChannels(Scheduler, commands.Cog): def report_stats(self) -> None: """Report the channel count stats.""" - total_in_use = len(list(self.get_category_channels(self.in_use_category))) - total_available = len(list(self.get_category_channels(self.available_category))) - total_dormant = len(list(self.get_category_channels(self.dormant_category))) + total_in_use = sum(1 for _ in self.get_category_channels(self.in_use_category)) + total_available = sum(1 for _ in self.get_category_channels(self.available_category)) + total_dormant = sum(1 for _ in self.get_category_channels(self.dormant_category)) self.bot.stats.gauge("help.total.in_use", total_in_use) self.bot.stats.gauge("help.total.available", total_available) -- cgit v1.2.3 From d5cd996864fe213d1c804911c2a17a6d04b8e170 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sun, 12 Apr 2020 01:00:27 +0100 Subject: Add a timeout to prevent the bot from being overloaded with presence updates --- bot/bot.py | 2 +- bot/cogs/stats.py | 12 ++++++++++-- bot/constants.py | 8 +++++++- config-default.yml | 4 +++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/bot/bot.py b/bot/bot.py index ef4a325dc..6dd5ba896 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -33,7 +33,7 @@ class Bot(commands.Bot): self._resolver = None self._guild_available = asyncio.Event() - statsd_url = constants.Bot.statsd_host + statsd_url = constants.Stats.statsd_host if DEBUG_MODE: # Since statsd is UDP, there are no errors for sending to a down port. diff --git a/bot/cogs/stats.py b/bot/cogs/stats.py index c15d0eb1b..df4827ba1 100644 --- a/bot/cogs/stats.py +++ b/bot/cogs/stats.py @@ -1,9 +1,10 @@ import string +from datetime import datetime from discord import Member, Message, Status from discord.ext.commands import Bot, Cog, Context -from bot.constants import Channels, Guild +from bot.constants import Channels, Guild, Stats as StatConf CHANNEL_NAME_OVERRIDES = { @@ -13,7 +14,7 @@ CHANNEL_NAME_OVERRIDES = { Channels.staff_lounge: "staff_lounge" } -ALLOWED_CHARS = string.ascii_letters + string.digits +ALLOWED_CHARS = string.ascii_letters + string.digits + "-" class Stats(Cog): @@ -21,6 +22,7 @@ class Stats(Cog): def __init__(self, bot: Bot): self.bot = bot + self.last_presence_update = None @Cog.listener() async def on_message(self, message: Message) -> None: @@ -73,6 +75,12 @@ class Stats(Cog): if after.guild.id != Guild.id: return + if self.last_presence_update: + if (datetime.now() - self.last_presence_update).seconds < StatConf.presence_update_timeout: + return + + self.last_presence_update = datetime.now() + online = 0 idle = 0 dnd = 0 diff --git a/bot/constants.py b/bot/constants.py index 33c1d530d..2add028e7 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -199,7 +199,6 @@ class Bot(metaclass=YAMLGetter): prefix: str token: str sentry_dsn: str - statsd_host: str class Filter(metaclass=YAMLGetter): section = "filter" @@ -351,6 +350,13 @@ class CleanMessages(metaclass=YAMLGetter): message_limit: int +class Stats(metaclass=YAMLGetter): + section = "bot" + subsection = "stats" + + presence_update_timeout: int + statsd_host: str + class Categories(metaclass=YAMLGetter): section = "guild" diff --git a/config-default.yml b/config-default.yml index 567caacbf..4cd61ce10 100644 --- a/config-default.yml +++ b/config-default.yml @@ -3,7 +3,9 @@ bot: token: !ENV "BOT_TOKEN" sentry_dsn: !ENV "BOT_SENTRY_DSN" - statsd_host: "graphite" + stats: + statsd_host: "graphite" + presence_update_timeout: 300 cooldowns: # Per channel, per tag. -- cgit v1.2.3 From a24f3736b37e8de9978931dc415d702d98f46b5c Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sun, 12 Apr 2020 01:18:48 +0100 Subject: Use underscore for metric names instead of dash --- bot/cogs/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/stats.py b/bot/cogs/stats.py index df4827ba1..d253db913 100644 --- a/bot/cogs/stats.py +++ b/bot/cogs/stats.py @@ -14,7 +14,7 @@ CHANNEL_NAME_OVERRIDES = { Channels.staff_lounge: "staff_lounge" } -ALLOWED_CHARS = string.ascii_letters + string.digits + "-" +ALLOWED_CHARS = string.ascii_letters + string.digits + "_" class Stats(Cog): -- cgit v1.2.3 From 540700694c7918058bf66af23a6351438423c155 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sun, 12 Apr 2020 01:48:58 +0100 Subject: timer -> timing for statsd --- bot/cogs/defcon.py | 2 +- bot/cogs/help_channels.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/cogs/defcon.py b/bot/cogs/defcon.py index 7043c7cbb..56fca002a 100644 --- a/bot/cogs/defcon.py +++ b/bot/cogs/defcon.py @@ -135,7 +135,7 @@ class Defcon(Cog): delta = datetime.now() - enabled - self.bot.stats.timer("defcon.enabled", delta) + self.bot.stats.timing("defcon.enabled", delta) except Exception: pass diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index d260a6a33..12bc4e279 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -469,7 +469,7 @@ class HelpChannels(Scheduler, commands.Cog): if channel.id in self.claim_times: claimed = self.claim_times[channel.id] in_use_time = datetime.now() - claimed - self.bot.stats.timer("help.in_use_time", in_use_time) + self.bot.stats.timing("help.in_use_time", in_use_time) log.trace(f"Position of #{channel} ({channel.id}) is actually {channel.position}.") -- cgit v1.2.3 From 7f1e65f51d6c597214bbdd7002d43afa4617e0c2 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Sun, 12 Apr 2020 11:12:01 +0100 Subject: [stat] Create a statistic for whether dormant was called by the claimant or staff --- bot/cogs/help_channels.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 4dd70d7bf..36ad6a7a3 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -198,10 +198,16 @@ class HelpChannels(Scheduler, commands.Cog): """Return True if the user is the help channel claimant or passes the role check.""" if self.help_channel_claimants.get(ctx.channel) == ctx.author: log.trace(f"{ctx.author} is the help channel claimant, passing the check for dormant.") + self.bot.stats.incr("help.dormant_invoke.claimant") return True log.trace(f"{ctx.author} is not the help channel claimant, checking roles.") - return with_role_check(ctx, *constants.HelpChannels.cmd_whitelist) + role_check = with_role_check(ctx, *constants.HelpChannels.cmd_whitelist) + + if role_check: + self.bot.stats.incr("help.dormant_invoke.staff") + + return role_check @commands.command(name="dormant", aliases=["close"], enabled=False) async def dormant_command(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From 3d25c790a69421e0ed9c7c7a29ca1d5833322169 Mon Sep 17 00:00:00 2001 From: Joseph Banks Date: Mon, 13 Apr 2020 03:20:43 +0100 Subject: [stat] Tag statistic was using the user input as the series name, not the resolved tag name --- bot/cogs/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index b81859db1..9ba33d7e0 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -208,7 +208,7 @@ class Tags(Cog): "channel": ctx.channel.id } - self.bot.stats.incr(f"tags.usages.{tag_name.replace('-', '_')}") + self.bot.stats.incr(f"tags.usages.{tag['title'].replace('-', '_')}") await wait_for_deletion( await ctx.send(embed=Embed.from_dict(tag['embed'])), -- cgit v1.2.3 From ea81f3b23192cf0840144cecfb2ca721c89a34fe Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Tue, 14 Apr 2020 11:04:55 +0200 Subject: Revert deletion of !dormant invocation messages PR #868 introduced the automatic deletion of the message that issued the `!dormant` command. The idea behind this was that moving the channel to the dormant category makes it obvious that a channel has gone dormant and the message would only serve as visual clutter. However, removing the command invocation also means that it's less obvious why a channel was moved to the dormant category. As the message gets deleted almost immediately, you have to be actively watching the channel to know that the command was issued and who issued it. This has already caused some confusion where helping members where left wondering why a channel suddenly went dormant while they felt that the conversation was still ongoing. To improve the user experience, this commit removes the deletions of the command invocation messages. --- bot/cogs/help_channels.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bot/cogs/help_channels.py b/bot/cogs/help_channels.py index 36ad6a7a3..692bb234c 100644 --- a/bot/cogs/help_channels.py +++ b/bot/cogs/help_channels.py @@ -224,10 +224,6 @@ class HelpChannels(Scheduler, commands.Cog): with suppress(KeyError): del self.help_channel_claimants[ctx.channel] - with suppress(discord.errors.NotFound): - log.trace("Deleting dormant invokation message.") - await ctx.message.delete() - with suppress(discord.errors.HTTPException, discord.errors.NotFound): await self.reset_claimant_send_permission(ctx.channel) -- cgit v1.2.3 From b44caed7c5b26f13b4b31d237ffe07f3157aadfb Mon Sep 17 00:00:00 2001 From: Sebastiaan Zeeff Date: Thu, 16 Apr 2020 16:21:25 +0200 Subject: Remove `.md` from anti-malware whitelist We want our members to use the paste site to share text-based files instead of them sharing the files as attachments on Discord. As `.md`, a file extensions used for plain-text files with markdown formatting, is such a text file, I've removed it from the anti-malware whitelist. --- config-default.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 4cd61ce10..f2b0bfa9f 100644 --- a/config-default.yml +++ b/config-default.yml @@ -475,7 +475,6 @@ anti_malware: - '.mp3' - '.wav' - '.ogg' - - '.md' reddit: -- cgit v1.2.3