We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bot/cogs/sync/cog.py`
Content:
```
1 import logging
2 from typing import Callable, Iterable
3 
4 from discord import Guild, Member, Role
5 from discord.ext import commands
6 from discord.ext.commands import Cog, Context
7 
8 from bot import constants
9 from bot.api import ResponseCodeError
10 from bot.bot import Bot
11 from bot.cogs.sync import syncers
12 
13 log = logging.getLogger(__name__)
14 
15 
16 class Sync(Cog):
17     """Captures relevant events and sends them to the site."""
18 
19     # The server to synchronize events on.
20     # Note that setting this wrongly will result in things getting deleted
21     # that possibly shouldn't be.
22     SYNC_SERVER_ID = constants.Guild.id
23 
24     # An iterable of callables that are called when the bot is ready.
25     ON_READY_SYNCERS: Iterable[Callable[[Bot, Guild], None]] = (
26         syncers.sync_roles,
27         syncers.sync_users
28     )
29 
30     def __init__(self, bot: Bot) -> None:
31         self.bot = bot
32 
33         self.bot.loop.create_task(self.sync_guild())
34 
35     async def sync_guild(self) -> None:
36         """Syncs the roles/users of the guild with the database."""
37         await self.bot.wait_until_ready()
38         guild = self.bot.get_guild(self.SYNC_SERVER_ID)
39         if guild is not None:
40             for syncer in self.ON_READY_SYNCERS:
41                 syncer_name = syncer.__name__[5:]  # drop off `sync_`
42                 log.info("Starting `%s` syncer.", syncer_name)
43                 total_created, total_updated, total_deleted = await syncer(self.bot, guild)
44                 if total_deleted is None:
45                     log.info(
46                         f"`{syncer_name}` syncer finished, created `{total_created}`, updated `{total_updated}`."
47                     )
48                 else:
49                     log.info(
50                         f"`{syncer_name}` syncer finished, created `{total_created}`, updated `{total_updated}`, "
51                         f"deleted `{total_deleted}`."
52                     )
53 
54     @Cog.listener()
55     async def on_guild_role_create(self, role: Role) -> None:
56         """Adds newly create role to the database table over the API."""
57         await self.bot.api_client.post(
58             'bot/roles',
59             json={
60                 'colour': role.colour.value,
61                 'id': role.id,
62                 'name': role.name,
63                 'permissions': role.permissions.value,
64                 'position': role.position,
65             }
66         )
67 
68     @Cog.listener()
69     async def on_guild_role_delete(self, role: Role) -> None:
70         """Deletes role from the database when it's deleted from the guild."""
71         await self.bot.api_client.delete(f'bot/roles/{role.id}')
72 
73     @Cog.listener()
74     async def on_guild_role_update(self, before: Role, after: Role) -> None:
75         """Syncs role with the database if any of the stored attributes were updated."""
76         if (
77                 before.name != after.name
78                 or before.colour != after.colour
79                 or before.permissions != after.permissions
80                 or before.position != after.position
81         ):
82             await self.bot.api_client.put(
83                 f'bot/roles/{after.id}',
84                 json={
85                     'colour': after.colour.value,
86                     'id': after.id,
87                     'name': after.name,
88                     'permissions': after.permissions.value,
89                     'position': after.position,
90                 }
91             )
92 
93     @Cog.listener()
94     async def on_member_join(self, member: Member) -> None:
95         """
96         Adds a new user or updates existing user to the database when a member joins the guild.
97 
98         If the joining member is a user that is already known to the database (i.e., a user that
99         previously left), it will update the user's information. If the user is not yet known by
100         the database, the user is added.
101         """
102         packed = {
103             'avatar_hash': member.avatar,
104             'discriminator': int(member.discriminator),
105             'id': member.id,
106             'in_guild': True,
107             'name': member.name,
108             'roles': sorted(role.id for role in member.roles)
109         }
110 
111         got_error = False
112 
113         try:
114             # First try an update of the user to set the `in_guild` field and other
115             # fields that may have changed since the last time we've seen them.
116             await self.bot.api_client.put(f'bot/users/{member.id}', json=packed)
117 
118         except ResponseCodeError as e:
119             # If we didn't get 404, something else broke - propagate it up.
120             if e.response.status != 404:
121                 raise
122 
123             got_error = True  # yikes
124 
125         if got_error:
126             # If we got `404`, the user is new. Create them.
127             await self.bot.api_client.post('bot/users', json=packed)
128 
129     @Cog.listener()
130     async def on_member_remove(self, member: Member) -> None:
131         """Updates the user information when a member leaves the guild."""
132         await self.bot.api_client.put(
133             f'bot/users/{member.id}',
134             json={
135                 'avatar_hash': member.avatar,
136                 'discriminator': int(member.discriminator),
137                 'id': member.id,
138                 'in_guild': False,
139                 'name': member.name,
140                 'roles': sorted(role.id for role in member.roles)
141             }
142         )
143 
144     @Cog.listener()
145     async def on_member_update(self, before: Member, after: Member) -> None:
146         """Updates the user information if any of relevant attributes have changed."""
147         if (
148                 before.name != after.name
149                 or before.avatar != after.avatar
150                 or before.discriminator != after.discriminator
151                 or before.roles != after.roles
152         ):
153             try:
154                 await self.bot.api_client.put(
155                     'bot/users/' + str(after.id),
156                     json={
157                         'avatar_hash': after.avatar,
158                         'discriminator': int(after.discriminator),
159                         'id': after.id,
160                         'in_guild': True,
161                         'name': after.name,
162                         'roles': sorted(role.id for role in after.roles)
163                     }
164                 )
165             except ResponseCodeError as e:
166                 if e.response.status != 404:
167                     raise
168 
169                 log.warning(
170                     "Unable to update user, got 404. "
171                     "Assuming race condition from join event."
172                 )
173 
174     @commands.group(name='sync')
175     @commands.has_permissions(administrator=True)
176     async def sync_group(self, ctx: Context) -> None:
177         """Run synchronizations between the bot and site manually."""
178 
179     @sync_group.command(name='roles')
180     @commands.has_permissions(administrator=True)
181     async def sync_roles_command(self, ctx: Context) -> None:
182         """Manually synchronize the guild's roles with the roles on the site."""
183         initial_response = await ctx.send("📊 Synchronizing roles.")
184         total_created, total_updated, total_deleted = await syncers.sync_roles(self.bot, ctx.guild)
185         await initial_response.edit(
186             content=(
187                 f"👌 Role synchronization complete, created **{total_created}** "
188                 f", updated **{total_created}** roles, and deleted **{total_deleted}** roles."
189             )
190         )
191 
192     @sync_group.command(name='users')
193     @commands.has_permissions(administrator=True)
194     async def sync_users_command(self, ctx: Context) -> None:
195         """Manually synchronize the guild's users with the users on the site."""
196         initial_response = await ctx.send("📊 Synchronizing users.")
197         total_created, total_updated, total_deleted = await syncers.sync_users(self.bot, ctx.guild)
198         await initial_response.edit(
199             content=(
200                 f"👌 User synchronization complete, created **{total_created}** "
201                 f"and updated **{total_created}** users."
202             )
203         )
204
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py
--- 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 @@
                         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 @@
 
     @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)

{"golden_diff": "diff --git a/bot/cogs/sync/cog.py b/bot/cogs/sync/cog.py\n--- a/bot/cogs/sync/cog.py\n+++ b/bot/cogs/sync/cog.py\n@@ -1,7 +1,7 @@\n import logging\n-from typing import Callable, Iterable\n+from typing import Callable, Dict, Iterable, Union\n \n-from discord import Guild, Member, Role\n+from discord import Guild, Member, Role, User\n from discord.ext import commands\n from discord.ext.commands import Cog, Context\n \n@@ -51,6 +51,15 @@\n                         f\"deleted `{total_deleted}`.\"\n                     )\n \n+    async def patch_user(self, user_id: int, updated_information: Dict[str, Union[str, int]]) -> None:\n+        \"\"\"Send a PATCH request to partially update a user in the database.\"\"\"\n+        try:\n+            await self.bot.api_client.patch(\"bot/users/\" + str(user_id), json=updated_information)\n+        except ResponseCodeError as e:\n+            if e.response.status != 404:\n+                raise\n+            log.warning(\"Unable to update user, got 404. Assuming race condition from join event.\")\n+\n     @Cog.listener()\n     async def on_guild_role_create(self, role: Role) -> None:\n         \"\"\"Adds newly create role to the database table over the API.\"\"\"\n@@ -143,33 +152,21 @@\n \n     @Cog.listener()\n     async def on_member_update(self, before: Member, after: Member) -> None:\n-        \"\"\"Updates the user information if any of relevant attributes have changed.\"\"\"\n-        if (\n-                before.name != after.name\n-                or before.avatar != after.avatar\n-                or before.discriminator != after.discriminator\n-                or before.roles != after.roles\n-        ):\n-            try:\n-                await self.bot.api_client.put(\n-                    'bot/users/' + str(after.id),\n-                    json={\n-                        'avatar_hash': after.avatar,\n-                        'discriminator': int(after.discriminator),\n-                        'id': after.id,\n-                        'in_guild': True,\n-                        'name': after.name,\n-                        'roles': sorted(role.id for role in after.roles)\n-                    }\n-                )\n-            except ResponseCodeError as e:\n-                if e.response.status != 404:\n-                    raise\n-\n-                log.warning(\n-                    \"Unable to update user, got 404. \"\n-                    \"Assuming race condition from join event.\"\n-                )\n+        \"\"\"Update the roles of the member in the database if a change is detected.\"\"\"\n+        if before.roles != after.roles:\n+            updated_information = {\"roles\": sorted(role.id for role in after.roles)}\n+            await self.patch_user(after.id, updated_information=updated_information)\n+\n+    @Cog.listener()\n+    async def on_user_update(self, before: User, after: User) -> None:\n+        \"\"\"Update the user information in the database if a relevant change is detected.\"\"\"\n+        if any(getattr(before, attr) != getattr(after, attr) for attr in (\"name\", \"discriminator\", \"avatar\")):\n+            updated_information = {\n+                \"name\": after.name,\n+                \"discriminator\": int(after.discriminator),\n+                \"avatar_hash\": after.avatar,\n+            }\n+            await self.patch_user(after.id, updated_information=updated_information)\n \n     @commands.group(name='sync')\n     @commands.has_permissions(administrator=True)\n", "issue": "Username, discriminator, and avatar changes are not synced with the database\nAt 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.\r\n\r\nThe 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.\r\n\n", "before_files": [{"content": "import logging\nfrom typing import Callable, Iterable\n\nfrom discord import Guild, Member, Role\nfrom discord.ext import commands\nfrom discord.ext.commands import Cog, Context\n\nfrom bot import constants\nfrom bot.api import ResponseCodeError\nfrom bot.bot import Bot\nfrom bot.cogs.sync import syncers\n\nlog = logging.getLogger(__name__)\n\n\nclass Sync(Cog):\n    \"\"\"Captures relevant events and sends them to the site.\"\"\"\n\n    # The server to synchronize events on.\n    # Note that setting this wrongly will result in things getting deleted\n    # that possibly shouldn't be.\n    SYNC_SERVER_ID = constants.Guild.id\n\n    # An iterable of callables that are called when the bot is ready.\n    ON_READY_SYNCERS: Iterable[Callable[[Bot, Guild], None]] = (\n        syncers.sync_roles,\n        syncers.sync_users\n    )\n\n    def __init__(self, bot: Bot) -> None:\n        self.bot = bot\n\n        self.bot.loop.create_task(self.sync_guild())\n\n    async def sync_guild(self) -> None:\n        \"\"\"Syncs the roles/users of the guild with the database.\"\"\"\n        await self.bot.wait_until_ready()\n        guild = self.bot.get_guild(self.SYNC_SERVER_ID)\n        if guild is not None:\n            for syncer in self.ON_READY_SYNCERS:\n                syncer_name = syncer.__name__[5:]  # drop off `sync_`\n                log.info(\"Starting `%s` syncer.\", syncer_name)\n                total_created, total_updated, total_deleted = await syncer(self.bot, guild)\n                if total_deleted is None:\n                    log.info(\n                        f\"`{syncer_name}` syncer finished, created `{total_created}`, updated `{total_updated}`.\"\n                    )\n                else:\n                    log.info(\n                        f\"`{syncer_name}` syncer finished, created `{total_created}`, updated `{total_updated}`, \"\n                        f\"deleted `{total_deleted}`.\"\n                    )\n\n    @Cog.listener()\n    async def on_guild_role_create(self, role: Role) -> None:\n        \"\"\"Adds newly create role to the database table over the API.\"\"\"\n        await self.bot.api_client.post(\n            'bot/roles',\n            json={\n                'colour': role.colour.value,\n                'id': role.id,\n                'name': role.name,\n                'permissions': role.permissions.value,\n                'position': role.position,\n            }\n        )\n\n    @Cog.listener()\n    async def on_guild_role_delete(self, role: Role) -> None:\n        \"\"\"Deletes role from the database when it's deleted from the guild.\"\"\"\n        await self.bot.api_client.delete(f'bot/roles/{role.id}')\n\n    @Cog.listener()\n    async def on_guild_role_update(self, before: Role, after: Role) -> None:\n        \"\"\"Syncs role with the database if any of the stored attributes were updated.\"\"\"\n        if (\n                before.name != after.name\n                or before.colour != after.colour\n                or before.permissions != after.permissions\n                or before.position != after.position\n        ):\n            await self.bot.api_client.put(\n                f'bot/roles/{after.id}',\n                json={\n                    'colour': after.colour.value,\n                    'id': after.id,\n                    'name': after.name,\n                    'permissions': after.permissions.value,\n                    'position': after.position,\n                }\n            )\n\n    @Cog.listener()\n    async def on_member_join(self, member: Member) -> None:\n        \"\"\"\n        Adds a new user or updates existing user to the database when a member joins the guild.\n\n        If the joining member is a user that is already known to the database (i.e., a user that\n        previously left), it will update the user's information. If the user is not yet known by\n        the database, the user is added.\n        \"\"\"\n        packed = {\n            'avatar_hash': member.avatar,\n            'discriminator': int(member.discriminator),\n            'id': member.id,\n            'in_guild': True,\n            'name': member.name,\n            'roles': sorted(role.id for role in member.roles)\n        }\n\n        got_error = False\n\n        try:\n            # First try an update of the user to set the `in_guild` field and other\n            # fields that may have changed since the last time we've seen them.\n            await self.bot.api_client.put(f'bot/users/{member.id}', json=packed)\n\n        except ResponseCodeError as e:\n            # If we didn't get 404, something else broke - propagate it up.\n            if e.response.status != 404:\n                raise\n\n            got_error = True  # yikes\n\n        if got_error:\n            # If we got `404`, the user is new. Create them.\n            await self.bot.api_client.post('bot/users', json=packed)\n\n    @Cog.listener()\n    async def on_member_remove(self, member: Member) -> None:\n        \"\"\"Updates the user information when a member leaves the guild.\"\"\"\n        await self.bot.api_client.put(\n            f'bot/users/{member.id}',\n            json={\n                'avatar_hash': member.avatar,\n                'discriminator': int(member.discriminator),\n                'id': member.id,\n                'in_guild': False,\n                'name': member.name,\n                'roles': sorted(role.id for role in member.roles)\n            }\n        )\n\n    @Cog.listener()\n    async def on_member_update(self, before: Member, after: Member) -> None:\n        \"\"\"Updates the user information if any of relevant attributes have changed.\"\"\"\n        if (\n                before.name != after.name\n                or before.avatar != after.avatar\n                or before.discriminator != after.discriminator\n                or before.roles != after.roles\n        ):\n            try:\n                await self.bot.api_client.put(\n                    'bot/users/' + str(after.id),\n                    json={\n                        'avatar_hash': after.avatar,\n                        'discriminator': int(after.discriminator),\n                        'id': after.id,\n                        'in_guild': True,\n                        'name': after.name,\n                        'roles': sorted(role.id for role in after.roles)\n                    }\n                )\n            except ResponseCodeError as e:\n                if e.response.status != 404:\n                    raise\n\n                log.warning(\n                    \"Unable to update user, got 404. \"\n                    \"Assuming race condition from join event.\"\n                )\n\n    @commands.group(name='sync')\n    @commands.has_permissions(administrator=True)\n    async def sync_group(self, ctx: Context) -> None:\n        \"\"\"Run synchronizations between the bot and site manually.\"\"\"\n\n    @sync_group.command(name='roles')\n    @commands.has_permissions(administrator=True)\n    async def sync_roles_command(self, ctx: Context) -> None:\n        \"\"\"Manually synchronize the guild's roles with the roles on the site.\"\"\"\n        initial_response = await ctx.send(\"\ud83d\udcca Synchronizing roles.\")\n        total_created, total_updated, total_deleted = await syncers.sync_roles(self.bot, ctx.guild)\n        await initial_response.edit(\n            content=(\n                f\"\ud83d\udc4c Role synchronization complete, created **{total_created}** \"\n                f\", updated **{total_created}** roles, and deleted **{total_deleted}** roles.\"\n            )\n        )\n\n    @sync_group.command(name='users')\n    @commands.has_permissions(administrator=True)\n    async def sync_users_command(self, ctx: Context) -> None:\n        \"\"\"Manually synchronize the guild's users with the users on the site.\"\"\"\n        initial_response = await ctx.send(\"\ud83d\udcca Synchronizing users.\")\n        total_created, total_updated, total_deleted = await syncers.sync_users(self.bot, ctx.guild)\n        await initial_response.edit(\n            content=(\n                f\"\ud83d\udc4c User synchronization complete, created **{total_created}** \"\n                f\"and updated **{total_created}** users.\"\n            )\n        )\n", "path": "bot/cogs/sync/cog.py"}], "after_files": [{"content": "import logging\nfrom typing import Callable, Dict, Iterable, Union\n\nfrom discord import Guild, Member, Role, User\nfrom discord.ext import commands\nfrom discord.ext.commands import Cog, Context\n\nfrom bot import constants\nfrom bot.api import ResponseCodeError\nfrom bot.bot import Bot\nfrom bot.cogs.sync import syncers\n\nlog = logging.getLogger(__name__)\n\n\nclass Sync(Cog):\n    \"\"\"Captures relevant events and sends them to the site.\"\"\"\n\n    # The server to synchronize events on.\n    # Note that setting this wrongly will result in things getting deleted\n    # that possibly shouldn't be.\n    SYNC_SERVER_ID = constants.Guild.id\n\n    # An iterable of callables that are called when the bot is ready.\n    ON_READY_SYNCERS: Iterable[Callable[[Bot, Guild], None]] = (\n        syncers.sync_roles,\n        syncers.sync_users\n    )\n\n    def __init__(self, bot: Bot) -> None:\n        self.bot = bot\n\n        self.bot.loop.create_task(self.sync_guild())\n\n    async def sync_guild(self) -> None:\n        \"\"\"Syncs the roles/users of the guild with the database.\"\"\"\n        await self.bot.wait_until_ready()\n        guild = self.bot.get_guild(self.SYNC_SERVER_ID)\n        if guild is not None:\n            for syncer in self.ON_READY_SYNCERS:\n                syncer_name = syncer.__name__[5:]  # drop off `sync_`\n                log.info(\"Starting `%s` syncer.\", syncer_name)\n                total_created, total_updated, total_deleted = await syncer(self.bot, guild)\n                if total_deleted is None:\n                    log.info(\n                        f\"`{syncer_name}` syncer finished, created `{total_created}`, updated `{total_updated}`.\"\n                    )\n                else:\n                    log.info(\n                        f\"`{syncer_name}` syncer finished, created `{total_created}`, updated `{total_updated}`, \"\n                        f\"deleted `{total_deleted}`.\"\n                    )\n\n    async def patch_user(self, user_id: int, updated_information: Dict[str, Union[str, int]]) -> None:\n        \"\"\"Send a PATCH request to partially update a user in the database.\"\"\"\n        try:\n            await self.bot.api_client.patch(\"bot/users/\" + str(user_id), json=updated_information)\n        except ResponseCodeError as e:\n            if e.response.status != 404:\n                raise\n            log.warning(\"Unable to update user, got 404. Assuming race condition from join event.\")\n\n    @Cog.listener()\n    async def on_guild_role_create(self, role: Role) -> None:\n        \"\"\"Adds newly create role to the database table over the API.\"\"\"\n        await self.bot.api_client.post(\n            'bot/roles',\n            json={\n                'colour': role.colour.value,\n                'id': role.id,\n                'name': role.name,\n                'permissions': role.permissions.value,\n                'position': role.position,\n            }\n        )\n\n    @Cog.listener()\n    async def on_guild_role_delete(self, role: Role) -> None:\n        \"\"\"Deletes role from the database when it's deleted from the guild.\"\"\"\n        await self.bot.api_client.delete(f'bot/roles/{role.id}')\n\n    @Cog.listener()\n    async def on_guild_role_update(self, before: Role, after: Role) -> None:\n        \"\"\"Syncs role with the database if any of the stored attributes were updated.\"\"\"\n        if (\n                before.name != after.name\n                or before.colour != after.colour\n                or before.permissions != after.permissions\n                or before.position != after.position\n        ):\n            await self.bot.api_client.put(\n                f'bot/roles/{after.id}',\n                json={\n                    'colour': after.colour.value,\n                    'id': after.id,\n                    'name': after.name,\n                    'permissions': after.permissions.value,\n                    'position': after.position,\n                }\n            )\n\n    @Cog.listener()\n    async def on_member_join(self, member: Member) -> None:\n        \"\"\"\n        Adds a new user or updates existing user to the database when a member joins the guild.\n\n        If the joining member is a user that is already known to the database (i.e., a user that\n        previously left), it will update the user's information. If the user is not yet known by\n        the database, the user is added.\n        \"\"\"\n        packed = {\n            'avatar_hash': member.avatar,\n            'discriminator': int(member.discriminator),\n            'id': member.id,\n            'in_guild': True,\n            'name': member.name,\n            'roles': sorted(role.id for role in member.roles)\n        }\n\n        got_error = False\n\n        try:\n            # First try an update of the user to set the `in_guild` field and other\n            # fields that may have changed since the last time we've seen them.\n            await self.bot.api_client.put(f'bot/users/{member.id}', json=packed)\n\n        except ResponseCodeError as e:\n            # If we didn't get 404, something else broke - propagate it up.\n            if e.response.status != 404:\n                raise\n\n            got_error = True  # yikes\n\n        if got_error:\n            # If we got `404`, the user is new. Create them.\n            await self.bot.api_client.post('bot/users', json=packed)\n\n    @Cog.listener()\n    async def on_member_remove(self, member: Member) -> None:\n        \"\"\"Updates the user information when a member leaves the guild.\"\"\"\n        await self.bot.api_client.put(\n            f'bot/users/{member.id}',\n            json={\n                'avatar_hash': member.avatar,\n                'discriminator': int(member.discriminator),\n                'id': member.id,\n                'in_guild': False,\n                'name': member.name,\n                'roles': sorted(role.id for role in member.roles)\n            }\n        )\n\n    @Cog.listener()\n    async def on_member_update(self, before: Member, after: Member) -> None:\n        \"\"\"Update the roles of the member in the database if a change is detected.\"\"\"\n        if before.roles != after.roles:\n            updated_information = {\"roles\": sorted(role.id for role in after.roles)}\n            await self.patch_user(after.id, updated_information=updated_information)\n\n    @Cog.listener()\n    async def on_user_update(self, before: User, after: User) -> None:\n        \"\"\"Update the user information in the database if a relevant change is detected.\"\"\"\n        if any(getattr(before, attr) != getattr(after, attr) for attr in (\"name\", \"discriminator\", \"avatar\")):\n            updated_information = {\n                \"name\": after.name,\n                \"discriminator\": int(after.discriminator),\n                \"avatar_hash\": after.avatar,\n            }\n            await self.patch_user(after.id, updated_information=updated_information)\n\n    @commands.group(name='sync')\n    @commands.has_permissions(administrator=True)\n    async def sync_group(self, ctx: Context) -> None:\n        \"\"\"Run synchronizations between the bot and site manually.\"\"\"\n\n    @sync_group.command(name='roles')\n    @commands.has_permissions(administrator=True)\n    async def sync_roles_command(self, ctx: Context) -> None:\n        \"\"\"Manually synchronize the guild's roles with the roles on the site.\"\"\"\n        initial_response = await ctx.send(\"\ud83d\udcca Synchronizing roles.\")\n        total_created, total_updated, total_deleted = await syncers.sync_roles(self.bot, ctx.guild)\n        await initial_response.edit(\n            content=(\n                f\"\ud83d\udc4c Role synchronization complete, created **{total_created}** \"\n                f\", updated **{total_created}** roles, and deleted **{total_deleted}** roles.\"\n            )\n        )\n\n    @sync_group.command(name='users')\n    @commands.has_permissions(administrator=True)\n    async def sync_users_command(self, ctx: Context) -> None:\n        \"\"\"Manually synchronize the guild's users with the users on the site.\"\"\"\n        initial_response = await ctx.send(\"\ud83d\udcca Synchronizing users.\")\n        total_created, total_updated, total_deleted = await syncers.sync_users(self.bot, ctx.guild)\n        await initial_response.edit(\n            content=(\n                f\"\ud83d\udc4c User synchronization complete, created **{total_created}** \"\n                f\"and updated **{total_created}** users.\"\n            )\n        )\n", "path": "bot/cogs/sync/cog.py"}]}