Repository: python-discord/bot
Issue: Author of a filtered message somehow becomes the bot itself

Sentry Issue: [BOT-7D](https://sentry.io/organizations/python-discord/issues/1801394207/?referrer=github_integration)

```
AttributeError: 'ClientUser' object has no attribute 'send'
  File "discord/client.py", line 312, in _run_event
    await coro(*args, **kwargs)
  File "bot/cogs/filtering.py", line 114, in on_message
    await self._filter_message(msg)
  File "bot/cogs/filtering.py", line 184, in _filter_message
    await self.notify_member(msg.author, _filter["notification_msg"], msg.channel)
  File "bot/cogs/filtering.py", line 366, in notify_member
    await filtered_member.send(reason)

Unhandled exception in on_message.
```

This is a very strange error. It was triggered by an invite link filter. Apparently the user left the guild quickly after sending the invite.

Unfortunately, the message object itself was filtered out by Sentry. Sentry shows that the match indeed had an invite link and that the author only had the `Developers` role. Furthermore, `_filter_messages` will exit early if the message's author is a bot. This means the bot definitely wasn't the author of the message. Yet somehow, the bot became the author when it tried to send a notification.

I suspect this is some esoteric discord.py cache or Discord API bug. Perhaps we should just check catch the `AttributeError` and move on. I really don't see anything wrong with our code. Does anyone else have insight into this?

Changed files:
bot/cogs/moderation/utils.py, bot/cogs/moderation/scheduler.py, bot/cogs/moderation/modlog.py, bot/cogs/antispam.py

diff --git a/bot/cogs/antispam.py b/bot/cogs/antispam.py
index 0bcca578..bc31cbd9 100644
--- a/bot/cogs/antispam.py
+++ b/bot/cogs/antispam.py
@@ -219,7 +219,6 @@ class AntiSpam(Cog):
             # Get context and make sure the bot becomes the actor of infraction by patching the `author` attributes
             context = await self.bot.get_context(msg)
             context.author = self.bot.user
-            context.message.author = self.bot.user
 
             # Since we're going to invoke the tempmute command directly, we need to manually call the converter.
             dt_remove_role_after = await self.expiration_date_converter.convert(context, f"{remove_role_after}S")
diff --git a/bot/cogs/moderation/modlog.py b/bot/cogs/moderation/modlog.py
index 0a63f57b..5f30d374 100644
--- a/bot/cogs/moderation/modlog.py
+++ b/bot/cogs/moderation/modlog.py
@@ -120,6 +120,10 @@ class ModLog(Cog, name="ModLog"):
             else:
                 content = "@everyone"
 
+        # Truncate content to 2000 characters and append an ellipsis.
+        if content and len(content) > 2000:
+            content = content[:2000 - 3] + "..."
+
         channel = self.bot.get_channel(channel_id)
         log_message = await channel.send(
             content=content,
diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py
index 75028d85..051f6c52 100644
--- a/bot/cogs/moderation/scheduler.py
+++ b/bot/cogs/moderation/scheduler.py
@@ -161,6 +161,7 @@ class InfractionScheduler:
                     self.schedule_expiration(infraction)
             except discord.HTTPException as e:
                 # Accordingly display that applying the infraction failed.
+                # Don't use ctx.message.author; antispam only patches ctx.author.
                 confirm_msg = ":x: failed to apply"
                 expiry_msg = ""
                 log_content = ctx.author.mention
@@ -190,6 +191,7 @@ class InfractionScheduler:
         await ctx.send(f"{dm_result}{confirm_msg}{infr_message}.")
 
         # Send a log message to the mod log.
+        # Don't use ctx.message.author for the actor; antispam only patches ctx.author.
         log.trace(f"Sending apply mod log for infraction #{id_}.")
         await self.mod_log.send_log_message(
             icon_url=icon,
@@ -198,7 +200,7 @@ class InfractionScheduler:
             thumbnail=user.avatar_url_as(static_format="png"),
             text=textwrap.dedent(f"""
                 Member: {user.mention} (`{user.id}`)
-                Actor: {ctx.message.author}{dm_log_text}{expiry_log_text}
+                Actor: {ctx.author}{dm_log_text}{expiry_log_text}
                 Reason: {reason}
             """),
             content=log_content,
@@ -242,7 +244,7 @@ class InfractionScheduler:
         log_text = await self.deactivate_infraction(response[0], send_log=False)
 
         log_text["Member"] = f"{user.mention}(`{user.id}`)"
-        log_text["Actor"] = str(ctx.message.author)
+        log_text["Actor"] = str(ctx.author)
         log_content = None
         id_ = response[0]['id']
         footer = f"ID: {id_}"
diff --git a/bot/cogs/moderation/utils.py b/bot/cogs/moderation/utils.py
index fb55287b..f2127210 100644
--- a/bot/cogs/moderation/utils.py
+++ b/bot/cogs/moderation/utils.py
@@ -70,7 +70,7 @@ async def post_infraction(
     log.trace(f"Posting {infr_type} infraction for {user} to the API.")
 
     payload = {
-        "actor": ctx.message.author.id,
+        "actor": ctx.author.id,  # Don't use ctx.message.author; antispam only patches ctx.author.
         "hidden": hidden,
         "reason": reason,
         "type": infr_type,