Repository: Cog-Creators/Red-DiscordBot

Issue:
No username in modlog entries
# Command bugs


#### Command name

hackban, reason

#### What cog is this command from?

Mod, ModLog

#### What were you expecting to happen?

ModLog entry should show a nickname and/or username.

#### What actually happened?

For users that aren't in the server, the ModLog entry shows no information indicating what user the entry is for.

#### How can we reproduce this issue?

Moderate or update a ModLog entry for any user not in the server or not in the bot's cache.

![modlog](https://user-images.githubusercontent.com/10516759/73109603-9d844600-3ed1-11ea-9f2a-33985f4bde61.png)


Discussion:
This is intended behavior. If the user isn't cached in the bot it will be unknown user. It was done to reduce the amount of fetch calls done to discord server to avoid 403s. 
Intentional, Modlog fetching user info was causing *rate-limit* issues. Fetching user info is an extremely limited operation compared to many others.
Could it not keep at least the user ID or username that is already in the modlog entry, instead of wiping it?
![image](https://user-images.githubusercontent.com/22035876/73116601-033ef500-3f07-11ea-885a-c02f6dc01102.png)

This image uses the non-embedded form but the scenarios do not differ between the two here.

In the first one, the user was in the bot's cache, so we kept the name and ID.
In the second one, the user wasn't in the bot's cache. We don't have the name and don't fetch it, but we keep the ID.
In the third one, we had both the name and id, but we discard the name to avoid modlog being used by bots to advertise, keeping the ID.

In no circumstance do we not at least store the ID. We are guaranteed to always have this when the bot is performing a ban.


My mistake. Misunderstood a piece of what was said when reading this before. The embedded form is differing in an unintended way and this is the issue, not the name not being present itself.
I'd like to reopen this issue as today I noticed a problem by following these steps:

1. Ban user without providing a reason. Username shows up in the modlog as expected.
2. Insert a `[p]reason`
3. The message gets edited with the reason and the username gets discarded, now showing `[Unknown or Deleted User] (ID)`

This makes the modlog entry itself less useful for future moderation checks and throws away information that was already present, which IMO is not a behaviour that one would reasonably expect. I'd rather have outdated information rather than no information at all.
Quick summary of discussion in Discord:

1. We can't just fetch the members on edit again.
2. We can accept some outdated info happening.
3. We shouldnt try to parse a modlog message

Which means we need modlog to store what was name#discrim that was available at the original action, if any and change how some of the updating happens.

Resolving patch:
diff --git a/redbot/core/modlog.py b/redbot/core/modlog.py
--- a/redbot/core/modlog.py
+++ b/redbot/core/modlog.py
@@ -214,12 +214,14 @@ def __init__(
         amended_by: Optional[discord.User] = None,
         modified_at: Optional[int] = None,
         message: Optional[discord.Message] = None,
+        last_known_username: Optional[str] = None,
     ):
         self.bot = bot
         self.guild = guild
         self.created_at = created_at
         self.action_type = action_type
         self.user = user
+        self.last_known_username = last_known_username
         self.moderator = moderator
         self.reason = reason
         self.until = until
@@ -241,10 +243,16 @@ async def edit(self, data: dict):
         """
         # We don't want case_number to be changed
         data.pop("case_number", None)
+        # last username is set based on passed user object
+        data.pop("last_known_username", None)
 
         for item in list(data.keys()):
             setattr(self, item, data[item])
 
+        # update last known username
+        if not isinstance(self.user, int):
+            self.last_known_username = f"{self.user.name}#{self.user.discriminator}"
+
         await _conf.custom(_CASES, str(self.guild.id), str(self.case_number)).set(self.to_json())
         self.bot.dispatch("modlog_case_edit", self)
         if not self.message:
@@ -314,7 +322,10 @@ async def message_content(self, embed: bool = True):
             )
 
         if isinstance(self.user, int):
-            user = f"[Unknown or Deleted User] ({self.user})"
+            if self.last_known_username is None:
+                user = f"[Unknown or Deleted User] ({self.user})"
+            else:
+                user = f"{self.last_known_username} ({self.user})"
             avatar_url = None
         else:
             user = escape_spoilers(
@@ -384,6 +395,7 @@ def to_json(self) -> dict:
             "guild": self.guild.id,
             "created_at": self.created_at,
             "user": user_id,
+            "last_known_username": self.last_known_username,
             "moderator": mod,
             "reason": self.reason,
             "until": self.until,
@@ -472,6 +484,7 @@ async def from_json(
             channel=channel,
             modified_at=data["modified_at"],
             message=message,
+            last_known_username=data.get("last_known_username"),
             **user_objects,
         )
 


Test patch: