diff options
| -rw-r--r-- | bot/cogs/moderation/infractions.py | 4 | ||||
| -rw-r--r-- | bot/cogs/moderation/management.py | 4 | ||||
| -rw-r--r-- | bot/cogs/moderation/utils.py | 75 | ||||
| -rw-r--r-- | bot/converters.py | 47 |
4 files changed, 92 insertions, 38 deletions
diff --git a/bot/cogs/moderation/infractions.py b/bot/cogs/moderation/infractions.py index 3536a3d38..253a8db5b 100644 --- a/bot/cogs/moderation/infractions.py +++ b/bot/cogs/moderation/infractions.py @@ -13,12 +13,10 @@ from bot.decorators import respect_role_hierarchy from bot.utils.checks import with_role_check from . import utils from .scheduler import InfractionScheduler -from .utils import MemberObject +from .utils import MemberObject, UserTypes as MemberConverter log = logging.getLogger(__name__) -MemberConverter = t.Union[utils.UserTypes, utils.proxy_user] - class Infractions(InfractionScheduler, commands.Cog): """Apply and pardon infractions on users for moderation purposes.""" diff --git a/bot/cogs/moderation/management.py b/bot/cogs/moderation/management.py index 9605d47b2..fff86e9ea 100644 --- a/bot/cogs/moderation/management.py +++ b/bot/cogs/moderation/management.py @@ -10,7 +10,7 @@ from discord.ext.commands import Context from bot import constants from bot.bot import Bot -from bot.converters import InfractionSearchQuery, allowed_strings +from bot.converters import InfractionSearchQuery, allowed_strings, proxy_user from bot.pagination import LinePaginator from bot.utils import time from bot.utils.checks import in_channel_check, with_role_check @@ -20,7 +20,7 @@ from .modlog import ModLog log = logging.getLogger(__name__) -UserConverter = t.Union[discord.User, utils.proxy_user] +UserConverter = t.Union[discord.User, proxy_user] class ModManagement(commands.Cog): diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py index 325b9567a..3b39b52ac 100644 --- a/bot/cogs/moderation/utils.py +++ b/bot/cogs/moderation/utils.py @@ -4,12 +4,11 @@ import typing as t from datetime import datetime import discord -from discord.ext import commands from discord.ext.commands import Context from bot.api import ResponseCodeError from bot.constants import Colours, Icons -from bot.converters import Duration, ISODateTime +from bot.converters import Duration, FetchedUser, ISODateTime log = logging.getLogger(__name__) @@ -25,30 +24,42 @@ INFRACTION_ICONS = { RULES_URL = "https://pythondiscord.com/pages/rules" APPEALABLE_INFRACTIONS = ("ban", "mute") -UserTypes = t.Union[discord.Member, discord.User] +UserTypes = t.Union[discord.Member, discord.User, FetchedUser] MemberObject = t.Union[UserTypes, discord.Object] Infraction = t.Dict[str, t.Union[str, int, bool]] Expiry = t.Union[Duration, ISODateTime] -def proxy_user(user_id: str) -> discord.Object: +async def post_user(ctx: Context, user: t.Union[discord.User, discord.Object]) -> t.Optional[dict]: """ - Create a proxy user object from the given id. + Create a new user in the database. - Used when a Member or User object cannot be resolved. + Used when an infraction needs to be applied on a user absent in the guild. """ - log.trace(f"Attempting to create a proxy user for the user id {user_id}.") + log.trace("Attempting to add user to the database.") - try: - user_id = int(user_id) - except ValueError: - raise commands.BadArgument + if not isinstance(user, discord.User): + log.warn("The given user is not a discord.User object.") - user = discord.Object(user_id) - user.mention = user.id - user.avatar_url_as = lambda static_format: None + payload = { + 'avatar_hash': getattr(user, 'avatar', 0), + 'discriminator': int(getattr(user, 'discriminator', 0)), + 'id': user.id, + 'in_guild': False, + 'name': getattr(user, 'name', 'Name unknown'), + 'roles': [] + } - return user + try: + response = await ctx.bot.api_client.post('bot/users', json=payload) + log.trace(f"User {user.id} added to the DB.") + return response + except ResponseCodeError as e: + log.warn("Couldn't post user.") + await ctx.send( + "The attempt to add the user to the DB failed: " + f"{e.status}, {e.response_text if e.response_text else 'no message received'}." + ) async def post_infraction( @@ -58,7 +69,7 @@ async def post_infraction( reason: str, expires_at: datetime = None, hidden: bool = False, - active: bool = True, + active: bool = True ) -> t.Optional[dict]: """Posts an infraction to the API.""" log.trace(f"Posting {infr_type} infraction for {user} to the API.") @@ -74,24 +85,22 @@ async def post_infraction( if expires_at: payload['expires_at'] = expires_at.isoformat() - try: - response = await ctx.bot.api_client.post('bot/infractions', json=payload) - except ResponseCodeError as exp: - if exp.status == 400 and 'user' in exp.response_json: - log.info( - f"{ctx.author} tried to add a {infr_type} infraction to `{user.id}`, " - "but that user id was not found in the database." - ) - await ctx.send( - f":x: Cannot add infraction, the specified user is not known to the database." - ) - return + # Try to apply the infraction. If it fails because the user doesn't exist, try to add it. + for should_post_user in (True, False): + try: + response = await ctx.bot.api_client.post('bot/infractions', json=payload) + return response + except ResponseCodeError as exp: + if exp.status == 400 and 'user'in exp.response_json: + # Only one attempt to add the user to the database, not two: + if not should_post_user or await post_user(ctx, user) is None: + return + else: + log.exception("An unexpected ResponseCodeError occurred while adding an infraction:") + await ctx.send(":x: There was an error adding the infraction.") + return else: - log.exception("An unexpected ResponseCodeError occurred while adding an infraction:") - await ctx.send(":x: There was an error adding the infraction.") - return - - return response + break async def has_active_infraction(ctx: Context, user: MemberObject, infr_type: str) -> bool: diff --git a/bot/converters.py b/bot/converters.py index 8d2ab7eb8..28bf58cf4 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -278,3 +278,50 @@ 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: + raise BadArgument + + user = discord.Object(user_id) + user.mention = user.id + user.avatar_url_as = lambda static_format: None + + return user + + +class FetchedUser(Converter): + """ + Fetches from the Discord API and returns a `discord.User` or `discord.Object` object, given an ID. + + If the fetching is successful, a `discord.User` object is returned. If it fails and + the error doesn't imply the user doesn't exist, then a `discord.Object` is returned + via the `user_proxy` function. + """ + + @staticmethod + async def convert(ctx: Context, user_id: str) -> t.Union[discord.User, discord.Object]: + """Convert `user_id` to a `discord.User` object, after fetching from the Discord API.""" + try: + user_id = int(user_id) + user = await ctx.bot.fetch_user(user_id) + except ValueError: + raise BadArgument(f"The provided argument can't be turned into integer: `{user_id}`") + except discord.HTTPException as e: + # If the Discord error isn't `Unknown user`, save it in the log and return a proxy instead + if e.code != 10013: + log.warning("Failed to fetch user, returning a proxy instead.") + return proxy_user(user_id) + raise BadArgument + + return user |