Repository: python-discord/bot

Issue:
Add checks to moderation commands to prevent banning the bot
We should prevent the bot from being able to infract itself!

Thanks to @decorator-factory for... discovering this one.

We need to add checks on moderation commands to check if the target user is the current bot.

Sentry Issue: [BOT-KS](https://sentry.io/organizations/python-discord/issues/2159129521/?referrer=github_integration)

```
AttributeError: 'ClientUser' object has no attribute 'create_dm'
(5 additional frame(s) were not displayed)
...
  File "bot/exts/moderation/infraction/_utils.py", line 179, in notify_infraction
    return await send_private_embed(user, embed)
  File "bot/exts/moderation/infraction/_utils.py", line 208, in send_private_embed
    await user.send(embed=embed)
  File "discord/abc.py", line 883, in send
    channel = await self._get_channel()
  File "discord/member.py", line 257, in _get_channel
    ch = await self.create_dm()
  File "discord/member.py", line 110, in general
    return getattr(self._user, x)(*args, **kwargs)

Error executing command invoked by fix error#1024: !ban <@!409107086526644234>
```
Add checks to moderation commands to prevent banning the bot
We should prevent the bot from being able to infract itself!

Thanks to @decorator-factory for... discovering this one.

We need to add checks on moderation commands to check if the target user is the current bot.

Sentry Issue: [BOT-KS](https://sentry.io/organizations/python-discord/issues/2159129521/?referrer=github_integration)

```
AttributeError: 'ClientUser' object has no attribute 'create_dm'
(5 additional frame(s) were not displayed)
...
  File "bot/exts/moderation/infraction/_utils.py", line 179, in notify_infraction
    return await send_private_embed(user, embed)
  File "bot/exts/moderation/infraction/_utils.py", line 208, in send_private_embed
    await user.send(embed=embed)
  File "discord/abc.py", line 883, in send
    channel = await self._get_channel()
  File "discord/member.py", line 257, in _get_channel
    ch = await self.create_dm()
  File "discord/member.py", line 110, in general
    return getattr(self._user, x)(*args, **kwargs)

Error executing command invoked by fix error#1024: !ban <@!409107086526644234>
```


Discussion:

Resolving patch:
diff --git a/bot/errors.py b/bot/errors.py
--- a/bot/errors.py
+++ b/bot/errors.py
@@ -1,4 +1,6 @@
-from typing import Hashable
+from typing import Hashable, Union
+
+from discord import Member, User
 
 
 class LockedResourceError(RuntimeError):
@@ -18,3 +20,18 @@ def __init__(self, resource_type: str, resource_id: Hashable):
             f"Cannot operate on {self.type.lower()} `{self.id}`; "
             "it is currently locked and in use by another operation."
         )
+
+
+class InvalidInfractedUser(Exception):
+    """
+    Exception raised upon attempt of infracting an invalid user.
+
+    Attributes:
+        `user` -- User or Member which is invalid
+    """
+
+    def __init__(self, user: Union[Member, User], reason: str = "User infracted is a bot."):
+        self.user = user
+        self.reason = reason
+
+        super().__init__(reason)
diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py
--- a/bot/exts/backend/error_handler.py
+++ b/bot/exts/backend/error_handler.py
@@ -12,7 +12,7 @@
 from bot.bot import Bot
 from bot.constants import Colours, ERROR_REPLIES, Icons, MODERATION_ROLES
 from bot.converters import TagNameConverter
-from bot.errors import LockedResourceError
+from bot.errors import InvalidInfractedUser, LockedResourceError
 from bot.exts.backend.branding._errors import BrandingError
 from bot.utils.checks import InWhitelistCheckFailure
 
@@ -82,6 +82,8 @@ async def on_command_error(self, ctx: Context, e: errors.CommandError) -> None:
             elif isinstance(e.original, BrandingError):
                 await ctx.send(embed=self._get_error_embed(random.choice(ERROR_REPLIES), str(e.original)))
                 return
+            elif isinstance(e.original, InvalidInfractedUser):
+                await ctx.send(f"Cannot infract that user. {e.original.reason}")
             else:
                 await self.handle_unexpected_error(ctx, e.original)
             return  # Exit early to avoid logging.
diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py
--- a/bot/exts/moderation/infraction/_utils.py
+++ b/bot/exts/moderation/infraction/_utils.py
@@ -7,6 +7,7 @@
 
 from bot.api import ResponseCodeError
 from bot.constants import Colours, Icons
+from bot.errors import InvalidInfractedUser
 
 log = logging.getLogger(__name__)
 
@@ -79,6 +80,10 @@ async def post_infraction(
     active: bool = True
 ) -> t.Optional[dict]:
     """Posts an infraction to the API."""
+    if isinstance(user, (discord.Member, discord.User)) and user.bot:
+        log.trace(f"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.")
+        raise InvalidInfractedUser(user)
+
     log.trace(f"Posting {infr_type} infraction for {user} to the API.")
 
     payload = {


Test patch: