diff options
| -rw-r--r-- | bot/cogs/alias.py | 17 | ||||
| -rw-r--r-- | bot/cogs/moderation/infractions.py | 5 | ||||
| -rw-r--r-- | bot/cogs/moderation/management.py | 4 | ||||
| -rw-r--r-- | bot/cogs/moderation/scheduler.py | 20 | ||||
| -rw-r--r-- | bot/cogs/moderation/utils.py | 68 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/bigbrother.py | 7 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/talentpool.py | 9 | ||||
| -rw-r--r-- | bot/cogs/watchchannels/watchchannel.py | 20 | ||||
| -rw-r--r-- | bot/converters.py | 52 |
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") |