aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/moderation/infractions.py4
-rw-r--r--bot/cogs/moderation/management.py4
-rw-r--r--bot/cogs/moderation/utils.py75
-rw-r--r--bot/converters.py47
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