aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/cogs/alias.py17
-rw-r--r--bot/cogs/moderation/infractions.py5
-rw-r--r--bot/cogs/moderation/management.py4
-rw-r--r--bot/cogs/moderation/scheduler.py20
-rw-r--r--bot/cogs/moderation/utils.py68
-rw-r--r--bot/cogs/watchchannels/bigbrother.py7
-rw-r--r--bot/cogs/watchchannels/talentpool.py9
-rw-r--r--bot/cogs/watchchannels/watchchannel.py20
-rw-r--r--bot/converters.py52
9 files changed, 127 insertions, 75 deletions
diff --git a/bot/cogs/alias.py b/bot/cogs/alias.py
index c1db38462..03c49c2f4 100644
--- a/bot/cogs/alias.py
+++ b/bot/cogs/alias.py
@@ -7,8 +7,7 @@ from discord.ext.commands import Cog, Command, Context, clean_content, command,
from bot.bot import Bot
from bot.cogs.extensions import Extension
-from bot.cogs.watchchannels.watchchannel import proxy_user
-from bot.converters import TagNameConverter
+from bot.converters import FetchedUser, TagNameConverter
from bot.pagination import LinePaginator
log = logging.getLogger(__name__)
@@ -61,12 +60,18 @@ class Alias (Cog):
await self.invoke(ctx, "site tools")
@command(name="watch", hidden=True)
- async def bigbrother_watch_alias(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None:
+ async def bigbrother_watch_alias(
+ self,
+ ctx: Context,
+ user: Union[Member, User, FetchedUser],
+ *,
+ reason: str
+ ) -> None:
"""Alias for invoking <prefix>bigbrother watch [user] [reason]."""
await self.invoke(ctx, "bigbrother watch", user, reason=reason)
@command(name="unwatch", hidden=True)
- async def bigbrother_unwatch_alias(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None:
+ async def bigbrother_unwatch_alias(self, ctx: Context, user: Union[User, FetchedUser], *, reason: str) -> None:
"""Alias for invoking <prefix>bigbrother unwatch [user] [reason]."""
await self.invoke(ctx, "bigbrother unwatch", user, reason=reason)
@@ -132,12 +137,12 @@ class Alias (Cog):
await self.invoke(ctx, "docs get", symbol)
@command(name="nominate", hidden=True)
- async def nomination_add_alias(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None:
+ async def nomination_add_alias(self, ctx: Context, user: Union[Member, User, FetchedUser], *, reason: str) -> None:
"""Alias for invoking <prefix>talentpool add [user] [reason]."""
await self.invoke(ctx, "talentpool add", user, reason=reason)
@command(name="unnominate", hidden=True)
- async def nomination_end_alias(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None:
+ async def nomination_end_alias(self, ctx: Context, user: Union[User, FetchedUser], *, reason: str) -> None:
"""Alias for invoking <prefix>nomination end [user] [reason]."""
await self.invoke(ctx, "nomination end", user, reason=reason)
diff --git a/bot/cogs/moderation/infractions.py b/bot/cogs/moderation/infractions.py
index 3536a3d38..5b6a63dbb 100644
--- a/bot/cogs/moderation/infractions.py
+++ b/bot/cogs/moderation/infractions.py
@@ -9,15 +9,16 @@ from discord.ext.commands import Context, command
from bot import constants
from bot.bot import Bot
from bot.constants import Event
+from bot.converters import FetchedUser
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
log = logging.getLogger(__name__)
-MemberConverter = t.Union[utils.UserTypes, utils.proxy_user]
+MemberConverter = t.Union[UserTypes, FetchedUser]
class Infractions(InfractionScheduler, commands.Cog):
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/scheduler.py b/bot/cogs/moderation/scheduler.py
index 01e4b1fe7..732091c17 100644
--- a/bot/cogs/moderation/scheduler.py
+++ b/bot/cogs/moderation/scheduler.py
@@ -106,16 +106,20 @@ class InfractionScheduler(Scheduler):
# DM the user about the infraction if it's not a shadow/hidden infraction.
if not infraction["hidden"]:
- # Sometimes user is a discord.Object; make it a proper user.
- user = await self.bot.fetch_user(user.id)
+ dm_result = f"{constants.Emojis.failmail} "
+ dm_log_text = "\nDM: **Failed**"
- # Accordingly display whether the user was successfully notified via DM.
- if await utils.notify_infraction(user, infr_type, expiry, reason, icon):
- dm_result = ":incoming_envelope: "
- dm_log_text = "\nDM: Sent"
+ # Sometimes user is a discord.Object; make it a proper user.
+ try:
+ if not isinstance(user, (discord.Member, discord.User)):
+ user = await self.bot.fetch_user(user.id)
+ except discord.HTTPException as e:
+ log.error(f"Failed to DM {user.id}: could not fetch user (status {e.status})")
else:
- dm_result = f"{constants.Emojis.failmail} "
- dm_log_text = "\nDM: **Failed**"
+ # Accordingly display whether the user was successfully notified via DM.
+ if await utils.notify_infraction(user, infr_type, expiry, reason, icon):
+ dm_result = ":incoming_envelope: "
+ dm_log_text = "\nDM: Sent"
if infraction["actor"] == self.bot.user.id:
log.trace(
diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py
index 325b9567a..73335ca30 100644
--- a/bot/cogs/moderation/utils.py
+++ b/bot/cogs/moderation/utils.py
@@ -4,7 +4,6 @@ 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
@@ -31,24 +30,33 @@ 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: MemberObject) -> 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(f"Attempting to add user {user.id} to the database.")
- try:
- user_id = int(user_id)
- except ValueError:
- raise commands.BadArgument
+ if not isinstance(user, (discord.Member, discord.User)):
+ log.warn("The user being added to the DB is not a Member or 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.info(f"User {user.id} added to the DB.")
+ return response
+ except ResponseCodeError as e:
+ log.error(f"Failed to add user {user.id} to the DB. {e}")
+ await ctx.send(f":x: The attempt to add the user to the DB failed: status {e.status}")
async def post_infraction(
@@ -58,7 +66,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 +82,20 @@ 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
- 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
+ # 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 e:
+ if e.status == 400 and 'user' in e.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(f"Unexpected error while adding an infraction for {user}:")
+ await ctx.send(f":x: There was an error adding the infraction: status {e.status}.")
+ return
async def has_active_infraction(ctx: Context, user: MemberObject, infr_type: str) -> bool:
diff --git a/bot/cogs/watchchannels/bigbrother.py b/bot/cogs/watchchannels/bigbrother.py
index 306ed4c64..7a30d5033 100644
--- a/bot/cogs/watchchannels/bigbrother.py
+++ b/bot/cogs/watchchannels/bigbrother.py
@@ -8,8 +8,9 @@ from discord.ext.commands import Cog, Context, group
from bot.bot import Bot
from bot.cogs.moderation.utils import post_infraction
from bot.constants import Channels, MODERATION_ROLES, Webhooks
+from bot.converters import FetchedUser
from bot.decorators import with_role
-from .watchchannel import WatchChannel, proxy_user
+from .watchchannel import WatchChannel
log = logging.getLogger(__name__)
@@ -46,7 +47,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
@bigbrother_group.command(name='watch', aliases=('w',))
@with_role(*MODERATION_ROLES)
- async def watch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None:
+ async def watch_command(self, ctx: Context, user: Union[User, FetchedUser], *, reason: str) -> None:
"""
Relay messages sent by the given `user` to the `#big-brother` channel.
@@ -93,7 +94,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"):
@bigbrother_group.command(name='unwatch', aliases=('uw',))
@with_role(*MODERATION_ROLES)
- async def unwatch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None:
+ async def unwatch_command(self, ctx: Context, user: Union[User, FetchedUser], *, reason: str) -> None:
"""Stop relaying messages by the given `user`."""
active_watches = await self.bot.api_client.get(
self.api_endpoint,
diff --git a/bot/cogs/watchchannels/talentpool.py b/bot/cogs/watchchannels/talentpool.py
index cc8feeeee..62be3bc3b 100644
--- a/bot/cogs/watchchannels/talentpool.py
+++ b/bot/cogs/watchchannels/talentpool.py
@@ -9,10 +9,11 @@ from discord.ext.commands import Cog, Context, group
from bot.api import ResponseCodeError
from bot.bot import Bot
from bot.constants import Channels, Guild, MODERATION_ROLES, STAFF_ROLES, Webhooks
+from bot.converters import FetchedUser
from bot.decorators import with_role
from bot.pagination import LinePaginator
from bot.utils import time
-from .watchchannel import WatchChannel, proxy_user
+from .watchchannel import WatchChannel
log = logging.getLogger(__name__)
@@ -49,7 +50,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
@nomination_group.command(name='watch', aliases=('w', 'add', 'a'))
@with_role(*STAFF_ROLES)
- async def watch_command(self, ctx: Context, user: Union[Member, User, proxy_user], *, reason: str) -> None:
+ async def watch_command(self, ctx: Context, user: Union[Member, User, FetchedUser], *, reason: str) -> None:
"""
Relay messages sent by the given `user` to the `#talent-pool` channel.
@@ -114,7 +115,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
@nomination_group.command(name='history', aliases=('info', 'search'))
@with_role(*MODERATION_ROLES)
- async def history_command(self, ctx: Context, user: Union[User, proxy_user]) -> None:
+ async def history_command(self, ctx: Context, user: Union[User, FetchedUser]) -> None:
"""Shows the specified user's nomination history."""
result = await self.bot.api_client.get(
self.api_endpoint,
@@ -143,7 +144,7 @@ class TalentPool(WatchChannel, Cog, name="Talentpool"):
@nomination_group.command(name='unwatch', aliases=('end', ))
@with_role(*MODERATION_ROLES)
- async def unwatch_command(self, ctx: Context, user: Union[User, proxy_user], *, reason: str) -> None:
+ async def unwatch_command(self, ctx: Context, user: Union[User, FetchedUser], *, reason: str) -> None:
"""
Ends the active nomination of the specified user with the given reason.
diff --git a/bot/cogs/watchchannels/watchchannel.py b/bot/cogs/watchchannels/watchchannel.py
index bd0622554..eb787b083 100644
--- a/bot/cogs/watchchannels/watchchannel.py
+++ b/bot/cogs/watchchannels/watchchannel.py
@@ -9,8 +9,8 @@ from typing import Optional
import dateutil.parser
import discord
-from discord import Color, Embed, HTTPException, Message, Object, errors
-from discord.ext.commands import BadArgument, Cog, Context
+from discord import Color, Embed, HTTPException, Message, errors
+from discord.ext.commands import Cog, Context
from bot.api import ResponseCodeError
from bot.bot import Bot
@@ -25,22 +25,6 @@ log = logging.getLogger(__name__)
URL_RE = re.compile(r"(https?://[^\s]+)")
-def proxy_user(user_id: str) -> Object:
- """A proxy user object that mocks a real User instance for when the later is not available."""
- try:
- user_id = int(user_id)
- except ValueError:
- raise BadArgument
-
- user = 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
-
-
@dataclass
class MessageHistory:
"""Represents a watch channel's message history."""
diff --git a/bot/converters.py b/bot/converters.py
index 8d2ab7eb8..a2e445d74 100644
--- a/bot/converters.py
+++ b/bot/converters.py
@@ -278,3 +278,55 @@ 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(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)
+ log.trace(f"Fetching user {user_id}...")
+ return await ctx.bot.fetch_user(user_id)
+ except ValueError:
+ log.debug(f"Failed to fetch user {user_id}: could not convert to int.")
+ 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(f"Failed to fetch user, returning a proxy instead: status {e.status}")
+ return proxy_user(user_id)
+
+ log.debug(f"Failed to fetch user {user_id}: user does not exist.")
+ raise BadArgument(f"User `{user_id}` does not exist")