Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support new username system #1025

Merged
merged 27 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5123152
docs: deprecate `discriminator` fields
shiftinv May 3, 2023
8f21161
docs: add docs to `Member.discriminator`/`.tag`
shiftinv May 5, 2023
218e3ba
feat: update `User.__str__` to handle `"0"` discriminator
shiftinv May 5, 2023
cda5bf9
feat: add `global_name` field
shiftinv May 6, 2023
c356a52
feat: update `display_name` handling
shiftinv May 6, 2023
049b44e
fix: update default avatar handling
shiftinv May 6, 2023
d031d6c
fix: assume widget members don't have a `global_name` for now
shiftinv May 6, 2023
430288d
fix: update converters and `get_member_named`
shiftinv May 6, 2023
73f0b55
docs: add changelog entry
shiftinv May 6, 2023
3ce1c9d
docs: cleanup/clarify/stuff/things
shiftinv May 6, 2023
c172608
fix: add `TeamMember.global_name` docs and repr
shiftinv May 6, 2023
a9acb1e
fix: include `global_name` in follower webhooks
shiftinv May 6, 2023
4269e8a
docs: fix broken reference
shiftinv May 7, 2023
56cbc15
fix(docs): it doesn't actually return the global name, oops
shiftinv May 9, 2023
f1000aa
docs: add more `versionchanged`
shiftinv May 9, 2023
2506c5c
Merge remote-tracking branch 'upstream/master' into feature/pomelo
shiftinv May 18, 2023
88b1e6e
docs: link to helpdesk article instead of not yet existent changelog …
shiftinv May 18, 2023
83d24ff
docs: un-deprecate discrims for now
shiftinv May 18, 2023
df79046
feat: support `username#0` in lookup methods
shiftinv May 20, 2023
c8d8d8d
fix: check for separator
shiftinv May 20, 2023
7a9392e
chore: update widget name stuff
shiftinv May 20, 2023
c7f05b0
docs: no `@` prefix
shiftinv May 20, 2023
bdc4a12
docs: improve gw member query docs
shiftinv May 20, 2023
841578c
fix: check for username instead of separator
shiftinv May 21, 2023
2407171
chore: remove todo
shiftinv May 21, 2023
de1d328
chore(docs): spacing
shiftinv May 21, 2023
44ad31b
Merge branch 'master' into feature/pomelo
onerandomusername May 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/1025.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Add support for new username system - see the official :ddocs:`changelog <change-log#unique-usernames-on-discord>` for details. Existing functionality is kept backwards-compatible while the migration is still ongoing.
onerandomusername marked this conversation as resolved.
Show resolved Hide resolved
- Add :attr:`User.global_name`, and update attributes/methods to account for it:
- :attr:`User.display_name` and :attr:`Member.display_name`
- :meth:`Guild.get_member_named`
- |commands| :class:`~ext.commands.UserConverter` and :class:`~ext.commands.MemberConverter`, now largely matching the behavior of :meth:`Guild.get_member_named`
- Update ``str(user)`` and ``str(member)`` to not include ``#0`` discriminator of migrated users.
- Adjust :attr:`User.default_avatar` to account for new default avatar handling.
14 changes: 14 additions & 0 deletions disnake/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,19 @@ class User(Snowflake, Protocol):
The user's username.
discriminator: :class:`str`
The user's discriminator.

.. deprecated:: 2.9
This is being phased out by Discord; the username system is moving away from ``username#discriminator``
to users having a globally unique ``@username``.
The value of a single zero (``"0"``) indicates that the user has been migrated to the new system.
See the :ddocs:`changelog <change-log#unique-usernames-on-discord>` for details.
shiftinv marked this conversation as resolved.
Show resolved Hide resolved
onerandomusername marked this conversation as resolved.
Show resolved Hide resolved
global_name: Optional[:class:`str`]
The user's global display name, if set.
This takes precedence over :attr:`.name` when shown.

For bots, this is the application name.

.. versionadded:: 2.9
shiftinv marked this conversation as resolved.
Show resolved Hide resolved
avatar: :class:`~disnake.Asset`
The avatar asset the user has.
bot: :class:`bool`
Expand All @@ -142,6 +155,7 @@ class User(Snowflake, Protocol):

name: str
discriminator: str
global_name: Optional[str]
avatar: Asset
bot: bool

Expand Down
37 changes: 29 additions & 8 deletions disnake/ext/commands/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,29 +188,40 @@ class MemberConverter(IDConverter[disnake.Member]):

1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name#discrim
4. Lookup by name
5. Lookup by nickname
3. Lookup by name#discrim.
4. Lookup by nickname.
5. Lookup by global name.
6. Lookup by name.

The name resolution order matches the one used by :meth:`.Guild.get_member_named`.

.. versionchanged:: 1.5
Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument`

.. versionchanged:: 1.5.1
This converter now lazily fetches members from the gateway and HTTP APIs,
optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled.

.. versionchanged:: 2.9
Name resolution order changed from ``name > nick`` to ``nick > global_name > name``
to account for the username migration.
"""

async def query_member_named(
self, guild: disnake.Guild, argument: str
) -> Optional[disnake.Member]:
cache = guild._state.member_cache_flags.joined
if len(argument) > 5 and argument[-5] == "#":
# legacy behavior for non-migrated users
username, _, discriminator = argument.rpartition("#")
members = await guild.query_members(username, limit=100, cache=cache)
return _utils_get(members, name=username, discriminator=discriminator)
else:
members = await guild.query_members(argument, limit=100, cache=cache)
return disnake.utils.find(lambda m: m.name == argument or m.nick == argument, members)
return disnake.utils.find(
lambda m: m.nick == argument or m.global_name == argument or m.name == argument,
members,
)

async def query_member_by_id(
self, bot: disnake.Client, guild: disnake.Guild, user_id: int
Expand Down Expand Up @@ -286,15 +297,19 @@ class UserConverter(IDConverter[disnake.User]):

1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name#discrim
4. Lookup by name
3. Lookup by name#discrim.
4. Lookup by global name.
5. Lookup by name.

.. versionchanged:: 1.5
Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument`

.. versionchanged:: 1.6
This converter now lazily fetches users from the HTTP APIs if an ID is passed
and it's not available in cache.

.. versionchanged:: 2.9
Now takes :attr:`~disnake.User.global_name` into account.
"""

async def convert(self, ctx: AnyContext, argument: str) -> disnake.User:
Expand Down Expand Up @@ -332,6 +347,7 @@ async def convert(self, ctx: AnyContext, argument: str) -> disnake.User:

# check for discriminator if it exists,
if len(arg) > 5 and arg[-5] == "#":
# legacy behavior for non-migrated users
discrim = arg[-4:]
name = arg[:-5]
result = disnake.utils.find(
Expand All @@ -340,7 +356,10 @@ async def convert(self, ctx: AnyContext, argument: str) -> disnake.User:
if result is not None:
return result

result = disnake.utils.find(lambda u: u.name == arg, state._users.values())
result = disnake.utils.find(
lambda u: u.global_name == arg or u.name == arg,
state._users.values(),
)

if result is None:
raise UserNotFound(argument)
Expand Down Expand Up @@ -1000,7 +1019,8 @@ class clean_content(Converter[str]):
fix_channel_mentions: :class:`bool`
Whether to clean channel mentions.
use_nicknames: :class:`bool`
Whether to use nicknames when transforming mentions.
Whether to use :attr:`nicknames <.Member.nick>` and
:attr:`global names <.Member.global_name>` when transforming mentions.
escape_markdown: :class:`bool`
Whether to also escape special markdown characters.
remove_markdown: :class:`bool`
Expand Down Expand Up @@ -1030,6 +1050,7 @@ def resolve_user(id: int) -> str:
m = (msg and _utils_get(msg.mentions, id=id)) or bot.get_user(id)
if m is None and ctx.guild:
m = ctx.guild.get_member(id)
# TODO: add a separate option for `global_name`s?
return f"@{m.display_name if self.use_nicknames else m.name}" if m else "@deleted-user"

def resolve_role(id: int) -> str:
Expand Down
1 change: 1 addition & 0 deletions disnake/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,7 @@ def members(self):
- :attr:`User.name`
- :attr:`User.avatar`
- :attr:`User.discriminator`
- :attr:`User.global_name`

For more information go to the :ref:`member intent documentation <need_members_intent>`.

Expand Down
27 changes: 16 additions & 11 deletions disnake/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,31 +1117,36 @@ def created_at(self) -> datetime.datetime:
def get_member_named(self, name: str, /) -> Optional[Member]:
"""Returns the first member found that matches the name provided.

The name can have an optional discriminator argument, e.g. "Jake#0001"
or "Jake" will both do the lookup. However the former will give a more
precise result. Note that the discriminator must have all 4 digits
for this to work.
The lookup strategy is as follows (in order):

If a nickname is passed, then it is looked up via the nickname. Note
however, that a nickname + discriminator combo will not lookup the nickname
but rather the username + discriminator combo due to nickname + discriminator
not being unique.
1. Lookup by nickname.
2. Lookup by global name.
3. Lookup by name.

While the migration away from discriminators is still ongoing,
the name can have an optional discriminator argument, e.g. "Jake#0001",
in which case it will be treated as a username + discriminator combo
(note: this only works with usernames, not nicknames).

If no member is found, ``None`` is returned.

.. versionchanged:: 2.9
Now takes :attr:`User.global_name` into account.

Parameters
----------
name: :class:`str`
The name of the member to lookup with an optional discriminator.
The name of the member to lookup (with an optional discriminator).

Returns
-------
Optional[:class:`Member`]
The member in this guild with the associated name. If not found
then ``None`` is returned.
"""
result = None
members = self.members

# legacy behavior for non-migrated users
if len(name) > 5 and name[-5] == "#":
# The 5 length is checking to see if #0000 is in the string,
# as a#0000 has a length of 6, the minimum for a potential
Expand All @@ -1155,7 +1160,7 @@ def get_member_named(self, name: str, /) -> Optional[Member]:
return result

def pred(m: Member) -> bool:
return m.nick == name or m.name == name
return m.nick == name or m.global_name == name or m.name == name

return utils.find(pred, members)

Expand Down
32 changes: 24 additions & 8 deletions disnake/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ class Member(disnake.abc.Messageable, _UserTag):

.. describe:: str(x)

Returns the member's name with the discriminator.
Returns the member's username (with discriminator, if not migrated to new system yet), or global name if set.

Attributes
----------
Expand All @@ -249,6 +249,7 @@ class Member(disnake.abc.Messageable, _UserTag):
The guild that the member belongs to.
nick: Optional[:class:`str`]
The guild specific nickname of the user.
This takes precedence over :attr:`.global_name` and :attr:`.name` when shown.
pending: :class:`bool`
Whether the member is pending member verification.

Expand Down Expand Up @@ -278,6 +279,7 @@ class Member(disnake.abc.Messageable, _UserTag):
if TYPE_CHECKING:
name: str
id: int
global_name: Optional[str]
bot: bool
system: bool
created_at: datetime.datetime
Expand Down Expand Up @@ -345,7 +347,7 @@ def __str__(self) -> str:

def __repr__(self) -> str:
return (
f"<Member id={self._user.id} name={self._user.name!r} discriminator={self._user.discriminator!r}"
f"<Member id={self._user.id} name={self._user.name!r} global_name={self._user.global_name!r} discriminator={self._user.discriminator!r}"
f" bot={self._user.bot} nick={self.nick!r} guild={self.guild!r}>"
)

Expand Down Expand Up @@ -449,17 +451,18 @@ def _presence_update(

def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
u = self._user
original = (u.name, u._avatar, u.discriminator, u._public_flags)
original = (u.name, u._avatar, u.discriminator, u.global_name, u._public_flags)
# These keys seem to always be available
modified = (
user["username"],
user["avatar"],
user["discriminator"],
user.get("global_name"),
user.get("public_flags", 0),
)
if original != modified:
to_return = User._copy(self._user)
u.name, u._avatar, u.discriminator, u._public_flags = modified
u.name, u._avatar, u.discriminator, u.global_name, u._public_flags = modified
# Signal to dispatch on_user_update
return to_return, u

Expand All @@ -483,10 +486,23 @@ def status(self, value: Status) -> None:

@property
def tag(self) -> str:
""":class:`str`: An alias of :attr:`.discriminator`.

.. deprecated:: 2.9
See :attr:`.discriminator`.
"""
return self._user.discriminator

@property
def discriminator(self) -> str:
""":class:`str`:The user's discriminator.

.. deprecated:: 2.9
This is being phased out by Discord; the username system is moving away from ``username#discriminator``
to users having a globally unique ``@username``.
The value of a single zero (``"0"``) indicates that the user has been migrated to the new system.
See the :ddocs:`changelog <change-log#unique-usernames-on-discord>` for details.
"""
return self._user.discriminator

@property
Expand Down Expand Up @@ -566,11 +582,11 @@ def mention(self) -> str:
def display_name(self) -> str:
""":class:`str`: Returns the user's display name.

For regular users this is just their username, but
if they have a guild specific nickname then that
is returned instead.
If they have a guild-specific :attr:`nickname <.nick>`, then
that is returned. If not, this is their :attr:`global name <.global_name>`
if set, or their :attr:`username <.name>` otherwise.
"""
return self.nick or self.name
return self.nick or self.global_name or self.name
shiftinv marked this conversation as resolved.
Show resolved Hide resolved

@property
def display_avatar(self) -> Asset:
Expand Down
19 changes: 15 additions & 4 deletions disnake/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class TeamMember(BaseUser):

.. describe:: str(x)

Returns the team member's name with discriminator.
Returns the team member's username (with discriminator, if not migrated to new system yet), or global name if set.

.. versionadded:: 1.3

Expand All @@ -95,7 +95,18 @@ class TeamMember(BaseUser):
id: :class:`int`
The team member's unique ID.
discriminator: :class:`str`
The team member's discriminator. This is given when the username has conflicts.
The team member's discriminator.

.. deprecated:: 2.9
This is being phased out by Discord; the username system is moving away from ``username#discriminator``
to users having a globally unique ``@username``.
The value of a single zero (``"0"``) indicates that the user has been migrated to the new system.
See the :ddocs:`changelog <change-log#unique-usernames-on-discord>` for details.
global_name: Optional[:class:`str`]
The team members's global display name, if set.
This takes precedence over :attr:`.name` when shown.
shiftinv marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 2.9
avatar: Optional[:class:`str`]
The avatar hash the team member has. Could be None.
bot: :class:`bool`
Expand All @@ -118,6 +129,6 @@ def __init__(self, team: Team, state: ConnectionState, data: TeamMemberPayload)

def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} id={self.id} name={self.name!r} "
f"discriminator={self.discriminator!r} membership_state={self.membership_state!r}>"
f"<{self.__class__.__name__} id={self.id} name={self.name!r} global_name={self.global_name!r}"
f" discriminator={self.discriminator!r} membership_state={self.membership_state!r}>"
)
5 changes: 4 additions & 1 deletion disnake/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from typing import Literal, Optional, TypedDict

from typing_extensions import NotRequired

from .snowflake import Snowflake


class PartialUser(TypedDict):
id: Snowflake
username: str
discriminator: str
discriminator: str # may be removed in future API versions
onerandomusername marked this conversation as resolved.
Show resolved Hide resolved
global_name: NotRequired[Optional[str]]
avatar: Optional[str]


Expand Down