Repository: python-discord/bot
Issue: Username, discriminator, and avatar changes are not synced with the database

At the moment, we do not synchronize changes in the username, discriminator, or avatar hash of our members with the database. The reason is that we only check for changes with an [`on_member_update` event listener](https://github.com/python-discord/bot/blob/a4f9d2a5323ca492e895ebd80a1515488b0c3e6c/bot/cogs/sync/cog.py#L145) and the changes listed above fire an [`on_user_update`](https://discordpy.readthedocs.io/en/latest/api.html#discord.on_user_update) event, not an [`on_member_update`](https://discordpy.readthedocs.io/en/latest/api.html#discord.on_member_update) event.

The solution is simple: Add an `on_user_update` event listener that looks for avatar, username, and discriminator changes and let the `on_member_update` look for changes in the roles of the member.


Changed files:
bot/cogs/sync/cog.py

diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py
index 90d4c40f..4e6ed156 100644
--- a/bot/cogs/sync/cog.py
+++ b/bot/cogs/sync/cog.py
@@ -1,7 +1,7 @@
 import logging
-from typing import Callable, Iterable
+from typing import Callable, Dict, Iterable, Union
 
-from discord import Guild, Member, Role
+from discord import Guild, Member, Role, User
 from discord.ext import commands
 from discord.ext.commands import Cog, Context
 
@@ -51,6 +51,15 @@ class Sync(Cog):
                         f"deleted `{total_deleted}`."
                     )
 
+    async def patch_user(self, user_id: int, updated_information: Dict[str, Union[str, int]]) -> None:
+        """Send a PATCH request to partially update a user in the database."""
+        try:
+            await self.bot.api_client.patch("bot/users/" + str(user_id), json=updated_information)
+        except ResponseCodeError as e:
+            if e.response.status != 404:
+                raise
+            log.warning("Unable to update user, got 404. Assuming race condition from join event.")
+
     @Cog.listener()
     async def on_guild_role_create(self, role: Role) -> None:
         """Adds newly create role to the database table over the API."""
@@ -143,33 +152,21 @@ class Sync(Cog):
 
     @Cog.listener()
     async def on_member_update(self, before: Member, after: Member) -> None:
-        """Updates the user information if any of relevant attributes have changed."""
-        if (
-                before.name != after.name
-                or before.avatar != after.avatar
-                or before.discriminator != after.discriminator
-                or before.roles != after.roles
-        ):
-            try:
-                await self.bot.api_client.put(
-                    'bot/users/' + str(after.id),
-                    json={
-                        'avatar_hash': after.avatar,
-                        'discriminator': int(after.discriminator),
-                        'id': after.id,
-                        'in_guild': True,
-                        'name': after.name,
-                        'roles': sorted(role.id for role in after.roles)
-                    }
-                )
-            except ResponseCodeError as e:
-                if e.response.status != 404:
-                    raise
-
-                log.warning(
-                    "Unable to update user, got 404. "
-                    "Assuming race condition from join event."
-                )
+        """Update the roles of the member in the database if a change is detected."""
+        if before.roles != after.roles:
+            updated_information = {"roles": sorted(role.id for role in after.roles)}
+            await self.patch_user(after.id, updated_information=updated_information)
+
+    @Cog.listener()
+    async def on_user_update(self, before: User, after: User) -> None:
+        """Update the user information in the database if a relevant change is detected."""
+        if any(getattr(before, attr) != getattr(after, attr) for attr in ("name", "discriminator", "avatar")):
+            updated_information = {
+                "name": after.name,
+                "discriminator": int(after.discriminator),
+                "avatar_hash": after.avatar,
+            }
+            await self.patch_user(after.id, updated_information=updated_information)
 
     @commands.group(name='sync')
     @commands.has_permissions(administrator=True)