diff options
Diffstat (limited to 'bot/converters.py')
-rw-r--r-- | bot/converters.py | 97 |
1 files changed, 94 insertions, 3 deletions
diff --git a/bot/converters.py b/bot/converters.py index cf0496541..cca57a02d 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -1,20 +1,39 @@ import logging import re +import typing as t from datetime import datetime from ssl import CertificateError -from typing import Union import dateutil.parser import dateutil.tz import discord from aiohttp import ClientConnectorError from dateutil.relativedelta import relativedelta -from discord.ext.commands import BadArgument, Context, Converter +from discord.ext.commands import BadArgument, Context, Converter, UserConverter log = logging.getLogger(__name__) +def allowed_strings(*values, preserve_case: bool = False) -> t.Callable[[str], str]: + """ + Return a converter which only allows arguments equal to one of the given values. + + Unless preserve_case is True, the argument is converted to lowercase. All values are then + expected to have already been given in lowercase too. + """ + def converter(arg: str) -> str: + if not preserve_case: + arg = arg.lower() + + if arg not in values: + raise BadArgument(f"Only the following values are allowed:\n```{', '.join(values)}```") + else: + return arg + + return converter + + class ValidPythonIdentifier(Converter): """ A converter that checks whether the given string is a valid Python identifier. @@ -70,7 +89,7 @@ class InfractionSearchQuery(Converter): """A converter that checks if the argument is a Discord user, and if not, falls back to a string.""" @staticmethod - async def convert(ctx: Context, arg: str) -> Union[discord.Member, str]: + async def convert(ctx: Context, arg: str) -> t.Union[discord.Member, str]: """Check if the argument is a Discord user, and if not, falls back to a string.""" try: maybe_snowflake = arg.strip("<@!>") @@ -259,3 +278,75 @@ class ISODateTime(Converter): dt = dt.replace(tzinfo=None) return dt + + +def proxy_user(user_id: str) -> discord.Object: + """ + Create a proxy user object from the given id. + + Used when a Member or User object cannot be resolved. + """ + log.trace(f"Attempting to create a proxy user for the user id {user_id}.") + + try: + user_id = int(user_id) + except ValueError: + log.debug(f"Failed to create proxy user {user_id}: could not convert to int.") + raise BadArgument(f"User ID `{user_id}` is invalid - could not convert to an integer.") + + user = discord.Object(user_id) + user.mention = user.id + user.display_name = f"<@{user.id}>" + user.avatar_url_as = lambda static_format: None + user.bot = False + + return user + + +class FetchedUser(UserConverter): + """ + Converts to a `discord.User` or, if it fails, a `discord.Object`. + + Unlike the default `UserConverter`, which only does lookups via the global user cache, this + converter attempts to fetch the user via an API call to Discord when the using the cache is + unsuccessful. + + If the fetch also fails and the error doesn't imply the user doesn't exist, then a + `discord.Object` is returned via the `user_proxy` converter. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name#discrim + 4. Lookup by name + 5. Lookup via API + 6. Create a proxy user with discord.Object + """ + + async def convert(self, ctx: Context, arg: str) -> t.Union[discord.User, discord.Object]: + """Convert the `arg` to a `discord.User` or `discord.Object`.""" + try: + return await super().convert(ctx, arg) + except BadArgument: + pass + + try: + user_id = int(arg) + log.trace(f"Fetching user {user_id}...") + return await ctx.bot.fetch_user(user_id) + except ValueError: + log.debug(f"Failed to fetch user {arg}: could not convert to int.") + raise BadArgument(f"The provided argument can't be turned into integer: `{arg}`") + except discord.HTTPException as e: + # If the Discord error isn't `Unknown user`, return a proxy instead + if e.code != 10013: + log.warning(f"Failed to fetch user, returning a proxy instead: status {e.status}") + return proxy_user(arg) + + log.debug(f"Failed to fetch user {arg}: user does not exist.") + raise BadArgument(f"User `{arg}` does not exist") + + +Expiry = t.Union[Duration, ISODateTime] +FetchedMember = t.Union[discord.Member, FetchedUser] |