diff options
Diffstat (limited to 'bot/exts/backend')
| -rw-r--r-- | bot/exts/backend/alias.py | 87 | ||||
| -rw-r--r-- | bot/exts/backend/error_handler.py | 3 | ||||
| -rw-r--r-- | bot/exts/backend/sync/_syncers.py | 173 |
3 files changed, 13 insertions, 250 deletions
diff --git a/bot/exts/backend/alias.py b/bot/exts/backend/alias.py deleted file mode 100644 index c6ba8d6f3..000000000 --- a/bot/exts/backend/alias.py +++ /dev/null @@ -1,87 +0,0 @@ -import inspect -import logging - -from discord import Colour, Embed -from discord.ext.commands import ( - Cog, Command, Context, - clean_content, command, group, -) - -from bot.bot import Bot -from bot.converters import TagNameConverter -from bot.pagination import LinePaginator - -log = logging.getLogger(__name__) - - -class Alias (Cog): - """Aliases for commonly used commands.""" - - def __init__(self, bot: Bot): - self.bot = bot - - async def invoke(self, ctx: Context, cmd_name: str, *args, **kwargs) -> None: - """Invokes a command with args and kwargs.""" - log.debug(f"{cmd_name} was invoked through an alias") - cmd = self.bot.get_command(cmd_name) - if not cmd: - return log.info(f'Did not find command "{cmd_name}" to invoke.') - elif not await cmd.can_run(ctx): - return log.info( - f'{str(ctx.author)} tried to run the command "{cmd_name}" but lacks permission.' - ) - - await ctx.invoke(cmd, *args, **kwargs) - - @command(name='aliases') - async def aliases_command(self, ctx: Context) -> None: - """Show configured aliases on the bot.""" - embed = Embed( - title='Configured aliases', - colour=Colour.blue() - ) - await LinePaginator.paginate( - ( - f"• `{ctx.prefix}{value.name}` " - f"=> `{ctx.prefix}{name[:-len('_alias')].replace('_', ' ')}`" - for name, value in inspect.getmembers(self) - if isinstance(value, Command) and name.endswith('_alias') - ), - ctx, embed, empty=False, max_lines=20 - ) - - @command(name="exception", hidden=True) - async def tags_get_traceback_alias(self, ctx: Context) -> None: - """Alias for invoking <prefix>tags get traceback.""" - await self.invoke(ctx, "tags get", tag_name="traceback") - - @group(name="get", - aliases=("show", "g"), - hidden=True, - invoke_without_command=True) - async def get_group_alias(self, ctx: Context) -> None: - """Group for reverse aliases for commands like `tags get`, allowing for `get tags` or `get docs`.""" - pass - - @get_group_alias.command(name="tags", aliases=("tag", "t"), hidden=True) - async def tags_get_alias( - self, ctx: Context, *, tag_name: TagNameConverter = None - ) -> None: - """ - Alias for invoking <prefix>tags get [tag_name]. - - tag_name: str - tag to be viewed. - """ - await self.invoke(ctx, "tags get", tag_name=tag_name) - - @get_group_alias.command(name="docs", aliases=("doc", "d"), hidden=True) - async def docs_get_alias( - self, ctx: Context, symbol: clean_content = None - ) -> None: - """Alias for invoking <prefix>docs get [symbol].""" - await self.invoke(ctx, "docs get", symbol) - - -def setup(bot: Bot) -> None: - """Load the Alias cog.""" - bot.add_cog(Alias(bot)) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index f9d4de638..c643d346e 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -10,6 +10,7 @@ from bot.api import ResponseCodeError from bot.bot import Bot from bot.constants import Channels, Colours from bot.converters import TagNameConverter +from bot.errors import LockedResourceError from bot.utils.checks import InWhitelistCheckFailure log = logging.getLogger(__name__) @@ -75,6 +76,8 @@ class ErrorHandler(Cog): elif isinstance(e, errors.CommandInvokeError): if isinstance(e.original, ResponseCodeError): await self.handle_api_error(ctx, e.original) + elif isinstance(e.original, LockedResourceError): + await ctx.send(f"{e.original} Please wait for it to finish and try again later.") else: await self.handle_unexpected_error(ctx, e.original) return # Exit early to avoid logging. diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py index f7ba811bc..3d4a09df3 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -1,15 +1,11 @@ import abc -import asyncio import logging import typing as t from collections import namedtuple -from functools import partial -import discord -from discord import Guild, HTTPException, Member, Message, Reaction, User +from discord import Guild from discord.ext.commands import Context -from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot @@ -25,9 +21,6 @@ _Diff = namedtuple('Diff', ('created', 'updated', 'deleted')) class Syncer(abc.ABC): """Base class for synchronising the database with objects in the Discord cache.""" - _CORE_DEV_MENTION = f"<@&{constants.Roles.core_developers}> " - _REACTION_EMOJIS = (constants.Emojis.check_mark, constants.Emojis.cross_mark) - def __init__(self, bot: Bot) -> None: self.bot = bot @@ -37,112 +30,6 @@ class Syncer(abc.ABC): """The name of the syncer; used in output messages and logging.""" raise NotImplementedError # pragma: no cover - async def _send_prompt(self, message: t.Optional[Message] = None) -> t.Optional[Message]: - """ - Send a prompt to confirm or abort a sync using reactions and return the sent message. - - If a message is given, it is edited to display the prompt and reactions. Otherwise, a new - message is sent to the dev-core channel and mentions the core developers role. If the - channel cannot be retrieved, return None. - """ - log.trace(f"Sending {self.name} sync confirmation prompt.") - - msg_content = ( - f'Possible cache issue while syncing {self.name}s. ' - f'More than {constants.Sync.max_diff} {self.name}s were changed. ' - f'React to confirm or abort the sync.' - ) - - # Send to core developers if it's an automatic sync. - if not message: - log.trace("Message not provided for confirmation; creating a new one in dev-core.") - channel = self.bot.get_channel(constants.Channels.dev_core) - - if not channel: - log.debug("Failed to get the dev-core channel from cache; attempting to fetch it.") - try: - channel = await self.bot.fetch_channel(constants.Channels.dev_core) - except HTTPException: - log.exception( - f"Failed to fetch channel for sending sync confirmation prompt; " - f"aborting {self.name} sync." - ) - return None - - allowed_roles = [discord.Object(constants.Roles.core_developers)] - message = await channel.send( - f"{self._CORE_DEV_MENTION}{msg_content}", - allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles) - ) - else: - await message.edit(content=msg_content) - - # Add the initial reactions. - log.trace(f"Adding reactions to {self.name} syncer confirmation prompt.") - for emoji in self._REACTION_EMOJIS: - await message.add_reaction(emoji) - - return message - - def _reaction_check( - self, - author: Member, - message: Message, - reaction: Reaction, - user: t.Union[Member, User] - ) -> bool: - """ - Return True if the `reaction` is a valid confirmation or abort reaction on `message`. - - If the `author` of the prompt is a bot, then a reaction by any core developer will be - considered valid. Otherwise, the author of the reaction (`user`) will have to be the - `author` of the prompt. - """ - # For automatic syncs, check for the core dev role instead of an exact author - has_role = any(constants.Roles.core_developers == role.id for role in user.roles) - return ( - reaction.message.id == message.id - and not user.bot - and (has_role if author.bot else user == author) - and str(reaction.emoji) in self._REACTION_EMOJIS - ) - - async def _wait_for_confirmation(self, author: Member, message: Message) -> bool: - """ - Wait for a confirmation reaction by `author` on `message` and return True if confirmed. - - Uses the `_reaction_check` function to determine if a reaction is valid. - - If there is no reaction within `bot.constants.Sync.confirm_timeout` seconds, return False. - To acknowledge the reaction (or lack thereof), `message` will be edited. - """ - # Preserve the core-dev role mention in the message edits so users aren't confused about - # where notifications came from. - mention = self._CORE_DEV_MENTION if author.bot else "" - - reaction = None - try: - log.trace(f"Waiting for a reaction to the {self.name} syncer confirmation prompt.") - reaction, _ = await self.bot.wait_for( - 'reaction_add', - check=partial(self._reaction_check, author, message), - timeout=constants.Sync.confirm_timeout - ) - except asyncio.TimeoutError: - # reaction will remain none thus sync will be aborted in the finally block below. - log.debug(f"The {self.name} syncer confirmation prompt timed out.") - - if str(reaction) == constants.Emojis.check_mark: - log.trace(f"The {self.name} syncer was confirmed.") - await message.edit(content=f':ok_hand: {mention}{self.name} sync will proceed.') - return True - else: - log.info(f"The {self.name} syncer was aborted or timed out!") - await message.edit( - content=f':warning: {mention}{self.name} sync aborted or timed out!' - ) - return False - @abc.abstractmethod async def _get_diff(self, guild: Guild) -> _Diff: """Return the difference between the cache of `guild` and the database.""" @@ -153,62 +40,19 @@ class Syncer(abc.ABC): """Perform the API calls for synchronisation.""" raise NotImplementedError # pragma: no cover - async def _get_confirmation_result( - self, - diff_size: int, - author: Member, - message: t.Optional[Message] = None - ) -> t.Tuple[bool, t.Optional[Message]]: - """ - Prompt for confirmation and return a tuple of the result and the prompt message. - - `diff_size` is the size of the diff of the sync. If it is greater than - `bot.constants.Sync.max_diff`, the prompt will be sent. The `author` is the invoked of the - sync and the `message` is an extant message to edit to display the prompt. - - If confirmed or no confirmation was needed, the result is True. The returned message will - either be the given `message` or a new one which was created when sending the prompt. - """ - log.trace(f"Determining if confirmation prompt should be sent for {self.name} syncer.") - if diff_size > constants.Sync.max_diff: - message = await self._send_prompt(message) - if not message: - return False, None # Couldn't get channel. - - confirmed = await self._wait_for_confirmation(author, message) - if not confirmed: - return False, message # Sync aborted. - - return True, message - async def sync(self, guild: Guild, ctx: t.Optional[Context] = None) -> None: """ Synchronise the database with the cache of `guild`. - If the differences between the cache and the database are greater than - `bot.constants.Sync.max_diff`, then a confirmation prompt will be sent to the dev-core - channel. The confirmation can be optionally redirect to `ctx` instead. + If `ctx` is given, send a message with the results. """ log.info(f"Starting {self.name} syncer.") - message = None - author = self.bot.user if ctx: message = await ctx.send(f"📊 Synchronising {self.name}s.") - author = ctx.author - + else: + message = None diff = await self._get_diff(guild) - diff_dict = diff._asdict() # Ugly method for transforming the NamedTuple into a dict - totals = {k: len(v) for k, v in diff_dict.items() if v is not None} - diff_size = sum(totals.values()) - - confirmed, message = await self._get_confirmation_result(diff_size, author, message) - if not confirmed: - return - - # Preserve the core-dev role mention in the message edits so users aren't confused about - # where notifications came from. - mention = self._CORE_DEV_MENTION if author.bot else "" try: await self._sync(diff) @@ -217,11 +61,14 @@ class Syncer(abc.ABC): # Don't show response text because it's probably some really long HTML. results = f"status {e.status}\n```{e.response_json or 'See log output for details'}```" - content = f":x: {mention}Synchronisation of {self.name}s failed: {results}" + content = f":x: Synchronisation of {self.name}s failed: {results}" else: - results = ", ".join(f"{name} `{total}`" for name, total in totals.items()) + diff_dict = diff._asdict() + results = (f"{name} `{len(val)}`" for name, val in diff_dict.items() if val is not None) + results = ", ".join(results) + log.info(f"{self.name} syncer finished: {results}.") - content = f":ok_hand: {mention}Synchronisation of {self.name}s complete: {results}" + content = f":ok_hand: Synchronisation of {self.name}s complete: {results}" if message: await message.edit(content=content) |