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") | 
