Repository: hikari-py/hikari
Pull request #1631 (2023-06-07 21:06:28)

Add new usernames
### Summary
Implementation of https://github.com/discord/discord-api-docs/pull/6218

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.




Patch:
diff --git a/changes/1631.deprecation.md b/changes/1631.deprecation.md
new file mode 100644
index 00000000..6e755113
--- /dev/null
+++ b/changes/1631.deprecation.md
@@ -0,0 +1,1 @@
+Deprecate `User.discriminator`
diff --git a/changes/1631.feature.md b/changes/1631.feature.md
new file mode 100644
index 00000000..e69113a5
--- /dev/null
+++ b/changes/1631.feature.md
@@ -0,0 +1,4 @@
+Implement username changes:
+- Add `global_name`
+- `User.__str__()` respects `global_name` now
+- `User.default_avatar_url` returns correct URL for migrated accounts
diff --git a/hikari/applications.py b/hikari/applications.py
index 811b2007..c60ab6ee 100644
--- a/hikari/applications.py
+++ b/hikari/applications.py
@@ -422,6 +422,10 @@ def mention(self) -> str:
     def username(self) -> str:
         return self.user.username
 
+    @property
+    def global_name(self) -> typing.Optional[str]:
+        return self.user.global_name
+
     def __str__(self) -> str:
         return str(self.user)
 
diff --git a/hikari/guilds.py b/hikari/guilds.py
index 09013e8e..599cab81 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -455,14 +455,16 @@ def display_name(self) -> str:
         """Return the member's display name.
 
         If the member has a nickname, this will return that nickname.
-        Otherwise, it will return the username instead.
+        If the user has a global name, this will return that global name.
+        If the user has neither, the username will be returned instead.
 
         See Also
         --------
         Nickname: `Member.nickname`.
         Username: `Member.username`.
+        Globalname: `Member.global_name`.
         """
-        return self.nickname if isinstance(self.nickname, str) else self.username
+        return self.nickname or self.global_name or self.username
 
     @property
     def flags(self) -> users.UserFlag:
@@ -572,6 +574,10 @@ def get_top_role(self) -> typing.Optional[Role]:
     def username(self) -> str:
         return self.user.username
 
+    @property
+    def global_name(self) -> typing.Optional[str]:
+        return self.user.global_name
+
     def make_avatar_url(self, *, ext: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[files.URL]:
         return self.user.make_avatar_url(ext=ext, size=size)
 
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index a98d5b9b..967afa53 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -230,6 +230,7 @@ class _UserFields:
     id: snowflakes.Snowflake = attrs.field()
     discriminator: str = attrs.field()
     username: str = attrs.field()
+    global_name: typing.Optional[str] = attrs.field()
     avatar_hash: str = attrs.field()
     banner_hash: typing.Optional[str] = attrs.field()
     accent_color: typing.Optional[color_models.Color] = attrs.field()
@@ -3519,6 +3520,7 @@ def _set_user_attrsibutes(payload: data_binding.JSONObject) -> _UserFields:
             id=snowflakes.Snowflake(payload["id"]),
             discriminator=payload["discriminator"],
             username=payload["username"],
+            global_name=payload.get("global_name"),
             avatar_hash=payload["avatar"],
             banner_hash=payload.get("banner", None),
             accent_color=color_models.Color(accent_color) if accent_color is not None else None,
@@ -3536,6 +3538,7 @@ def deserialize_user(self, payload: data_binding.JSONObject) -> user_models.User
             id=user_fields.id,
             discriminator=user_fields.discriminator,
             username=user_fields.username,
+            global_name=payload.get("global_name"),
             avatar_hash=user_fields.avatar_hash,
             banner_hash=user_fields.banner_hash,
             accent_color=user_fields.accent_color,
@@ -3551,6 +3554,7 @@ def deserialize_my_user(self, payload: data_binding.JSONObject) -> user_models.O
             id=user_fields.id,
             discriminator=user_fields.discriminator,
             username=user_fields.username,
+            global_name=payload.get("global_name"),
             avatar_hash=user_fields.avatar_hash,
             banner_hash=user_fields.banner_hash,
             accent_color=user_fields.accent_color,
diff --git a/hikari/impl/event_factory.py b/hikari/impl/event_factory.py
index 339586ee..5e638107 100644
--- a/hikari/impl/event_factory.py
+++ b/hikari/impl/event_factory.py
@@ -468,6 +468,7 @@ def deserialize_presence_update_event(
                 id=snowflakes.Snowflake(user_payload["id"]),
                 discriminator=discriminator,
                 username=user_payload.get("username", undefined.UNDEFINED),
+                global_name=user_payload.get("global_name", undefined.UNDEFINED),
                 avatar_hash=user_payload.get("avatar", undefined.UNDEFINED),
                 banner_hash=user_payload.get("banner", undefined.UNDEFINED),
                 accent_color=accent_color,
diff --git a/hikari/impl/shard.py b/hikari/impl/shard.py
index 5e4427aa..97d6ff42 100644
--- a/hikari/impl/shard.py
+++ b/hikari/impl/shard.py
@@ -725,10 +725,14 @@ async def _poll_events(self) -> None:
                     user_pl = data["user"]
                     self._user_id = snowflakes.Snowflake(user_pl["id"])
 
+                    # TODO: Remove when rollout finishes
+                    user = user_pl["username"] + (
+                        "#" + user_pl["discriminator"] if user_pl["discriminator"] != "0" else ""
+                    )
                     self._logger.info(
                         "shard is ready: %s guilds, %s (%s), session %r on v%s gateway",
                         len(data["guilds"]),
-                        f"{user_pl['username']}#{user_pl['discriminator']}",
+                        user,
                         self._user_id,
                         self._session_id,
                         data["v"],
diff --git a/hikari/users.py b/hikari/users.py
index aa760da2..a993f30f 100644
--- a/hikari/users.py
+++ b/hikari/users.py
@@ -166,13 +166,24 @@ def accent_colour(self) -> undefined.UndefinedNoneOr[colors.Color]:
     @property
     @abc.abstractmethod
     def discriminator(self) -> undefined.UndefinedOr[str]:
-        """Discriminator for the user."""
+        """Discriminator for the user.
+
+        .. deprecation:: 2.0.0.dev120
+            Discriminators are deprecated and being replaced with "0" by Discord
+            during username migration. This field will be removed after migration is complete.
+            Learn more here: https://dis.gd/usernames
+        """
 
     @property
     @abc.abstractmethod
     def username(self) -> undefined.UndefinedOr[str]:
         """Username for the user."""
 
+    @property
+    @abc.abstractmethod
+    def global_name(self) -> undefined.UndefinedNoneOr[str]:
+        """Global name for the user, if they have one, otherwise `None`."""
+
     @property
     @abc.abstractmethod
     def is_bot(self) -> undefined.UndefinedOr[bool]:
@@ -484,6 +495,11 @@ def banner_url(self) -> typing.Optional[files.URL]:
     @property
     def default_avatar_url(self) -> files.URL:
         """Default avatar URL for this user."""
+        if self.discriminator == "0":  # migrated account
+            return routes.CDN_DEFAULT_USER_AVATAR.compile_to_file(
+                urls.CDN_URL, discriminator=self.id.created_at.timestamp() % 6, file_format="png"
+            )
+
         return routes.CDN_DEFAULT_USER_AVATAR.compile_to_file(
             urls.CDN_URL, discriminator=int(self.discriminator) % 5, file_format="png"
         )
@@ -496,7 +512,13 @@ def display_avatar_url(self) -> files.URL:
     @property
     @abc.abstractmethod
     def discriminator(self) -> str:
-        """Discriminator for the user."""
+        """Discriminator for the user.
+
+        .. deprecation:: 2.0.0.dev120
+            Discriminators are deprecated and being replaced with "0" by Discord
+            during username migration. This field will be removed after migration is complete.
+            Learn more here: https://dis.gd/usernames
+        """
 
     @property
     @abc.abstractmethod
@@ -531,6 +553,11 @@ def mention(self) -> str:
     def username(self) -> str:
         """Username for the user."""
 
+    @property
+    @abc.abstractmethod
+    def global_name(self) -> typing.Optional[str]:
+        """Global name for the user, if they have one, otherwise `None`."""
+
     def make_avatar_url(self, *, ext: typing.Optional[str] = None, size: int = 4096) -> typing.Optional[files.URL]:
         """Generate the avatar URL for this user, if set.
 
@@ -636,11 +663,20 @@ class PartialUserImpl(PartialUser):
     """Client application that models may use for procedures."""
 
     discriminator: undefined.UndefinedOr[str] = attrs.field(eq=False, hash=False, repr=True)
-    """Four-digit discriminator for the user."""
+    """Four-digit discriminator for the user if unmigrated.
+
+    .. deprecation:: 2.0.0.dev120
+        Discriminators are deprecated and being replaced with "0" by Discord
+        during username migration. This field will be removed after migration is complete.
+        Learn more here: https://dis.gd/usernames
+    """
 
     username: undefined.UndefinedOr[str] = attrs.field(eq=False, hash=False, repr=True)
     """Username of the user."""
 
+    global_name: undefined.UndefinedNoneOr[str] = attrs.field(eq=False, hash=False, repr=True)
+    """Global name of the user."""
+
     avatar_hash: undefined.UndefinedNoneOr[str] = attrs.field(eq=False, hash=False, repr=False)
     """Avatar hash of the user, if a custom avatar is set."""
 
@@ -678,6 +714,8 @@ def mention(self) -> str:
     def __str__(self) -> str:
         if self.username is undefined.UNDEFINED or self.discriminator is undefined.UNDEFINED:
             return f"Partial user ID {self.id}"
+        elif self.discriminator == "0":  # migrated account
+            return self.username
         return f"{self.username}#{self.discriminator}"
 
 
@@ -686,11 +724,20 @@ class UserImpl(PartialUserImpl, User):
     """Concrete implementation of user information."""
 
     discriminator: str = attrs.field(eq=False, hash=False, repr=True)
-    """The user's discriminator."""
+    """The user's discriminator.
+
+    .. deprecation:: 2.0.0.dev120
+        Discriminators are deprecated and being replaced with "0" by Discord
+        during username migration. This field will be removed after migration is complete.
+        Learn more here: https://dis.gd/usernames
+    """
 
     username: str = attrs.field(eq=False, hash=False, repr=True)
     """The user's username."""
 
+    global_name: typing.Optional[str] = attrs.field(eq=False, hash=False, repr=True)
+    """The user's global name."""
+
     avatar_hash: typing.Optional[str] = attrs.field(eq=False, hash=False, repr=False)
     """The user's avatar hash, if they have one, otherwise `None`."""
 
diff --git a/scripts/gateway_stub.py b/scripts/gateway_stub.py
index 15a5f010..90f75867 100644
--- a/scripts/gateway_stub.py
+++ b/scripts/gateway_stub.py
@@ -36,6 +36,7 @@
 
 me_user_id = "1234567890"
 me_username = "nekokatt"
+me_global_name = None
 me_discriminator = "6945"
 me_avatar = None
 me_bot = True
@@ -71,6 +72,7 @@ def v8_get_my_user(_):
     body = {
         "id": me_user_id,
         "username": me_username,
+        "global_name": me_global_name,
         "discriminator": me_discriminator,
         "avatar": me_avatar,
         "bot": me_bot,


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 468a79f9..1706e820 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -6692,6 +6692,7 @@ def my_user_payload(self):
         return {
             "id": "379953393319542784",
             "username": "qt pi",
+            "global_name": "blahaj",
             "avatar": "820d0e50543216e812ad94e6ab7",
             "banner": "a_221313e1e2edsncsncsmcndsc",
             "accent_color": 231321,
@@ -6712,6 +6713,7 @@ def test_deserialize_my_user(self, entity_factory_impl, mock_app, my_user_payloa
         assert my_user.app is mock_app
         assert my_user.id == 379953393319542784
         assert my_user.username == "qt pi"
+        assert my_user.global_name == "blahaj"
         assert my_user.avatar_hash == "820d0e50543216e812ad94e6ab7"
         assert my_user.banner_hash == "a_221313e1e2edsncsncsmcndsc"
         assert my_user.accent_color == 231321
@@ -6740,6 +6742,7 @@ def test_deserialize_my_user_with_unset_fields(self, entity_factory_impl, mock_a
                 "premium_type": 1,
             }
         )
+        assert my_user.global_name is None
         assert my_user.app is mock_app
         assert my_user.banner_hash is None
         assert my_user.accent_color is None
diff --git a/tests/hikari/impl/test_event_factory.py b/tests/hikari/impl/test_event_factory.py
index 9d463344..1cb96682 100644
--- a/tests/hikari/impl/test_event_factory.py
+++ b/tests/hikari/impl/test_event_factory.py
@@ -651,6 +651,7 @@ def test_deserialize_presence_update_event_with_full_user_object(self, event_fac
             "user": {
                 "id": "1231312",
                 "username": "OK",
+                "global_name": "blahaj",
                 "avatar": "NOK",
                 "banner": "12122hssjamanmdd",
                 "accent_color": 12342,
@@ -674,6 +675,7 @@ def test_deserialize_presence_update_event_with_full_user_object(self, event_fac
         assert isinstance(event.user, user_models.PartialUser)
         assert event.user.id == 1231312
         assert event.user.username == "OK"
+        assert event.user.global_name == "blahaj"
         assert event.user.discriminator == "1231"
         assert event.user.avatar_hash == "NOK"
         assert event.user.banner_hash == "12122hssjamanmdd"
@@ -701,6 +703,7 @@ def test_deserialize_presence_update_event_with_partial_user_object(self, event_
         assert isinstance(event.user, user_models.PartialUser)
         assert event.user.id == 1231312
         assert event.user.username is undefined.UNDEFINED
+        assert event.user.global_name is undefined.UNDEFINED
         assert event.user.discriminator is undefined.UNDEFINED
         assert event.user.avatar_hash is undefined.UNDEFINED
         assert event.user.banner_hash is undefined.UNDEFINED
diff --git a/tests/hikari/integration/test_users_comparison.py b/tests/hikari/integration/test_users_comparison.py
index a2f28f98..f5227c72 100644
--- a/tests/hikari/integration/test_users_comparison.py
+++ b/tests/hikari/integration/test_users_comparison.py
@@ -49,6 +49,7 @@ def make_user(user_id, username):
         id=snowflakes.Snowflake(user_id),
         discriminator="0001",
         username=username,
+        global_name=None,
         avatar_hash=None,
         banner_hash=None,
         accent_color=None,
diff --git a/tests/hikari/test_guilds.py b/tests/hikari/test_guilds.py
index 5142fa56..1c07054b 100644
--- a/tests/hikari/test_guilds.py
+++ b/tests/hikari/test_guilds.py
@@ -459,7 +459,7 @@ def test_display_name_property_when_nickname(self, model):
 
     def test_display_name_property_when_no_nickname(self, model, mock_user):
         model.nickname = None
-        assert model.display_name is mock_user.username
+        assert model.display_name is mock_user.global_name
 
     def test_mention_property(self, model, mock_user):
         assert model.mention == mock_user.mention
diff --git a/tests/hikari/test_users.py b/tests/hikari/test_users.py
index fde0c267..681ca54c 100644
--- a/tests/hikari/test_users.py
+++ b/tests/hikari/test_users.py
@@ -313,6 +313,7 @@ def obj(self):
             app=mock.Mock(),
             discriminator="8637",
             username="thomm.o",
+            global_name=None,
             avatar_hash=None,
             banner_hash=None,
             accent_color=None,
@@ -348,6 +349,7 @@ def obj(self):
             app=mock.Mock(),
             discriminator="1234",
             username="foobar",
+            global_name=None,
             avatar_hash="69420",
             banner_hash="42069",
             accent_color=123456,