Skip to content

feat: support new username system #1025

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

Merged
merged 27 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from 23 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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 `help article <https://dis.gd/app-usernames>`__ for details. Existing functionality is kept backwards-compatible while the migration is still ongoing.
- 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.

.. note::
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 `help article <https://dis.gd/app-usernames>`__ for details.
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
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
74 changes: 47 additions & 27 deletions disnake/ext/commands/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,31 +186,46 @@ class MemberConverter(IDConverter[disnake.Member]):

The lookup strategy is as follows (in order):

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

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 ``username > nick`` to
``nick > global_name > username`` 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] == "#":
username, _, discriminator = argument.rpartition("#")

username, sep, discriminator = argument.rpartition("#")
if sep and (
discriminator == "0" or (len(discriminator) == 4 and discriminator.isdecimal())
):
# legacy behavior
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 @@ -284,17 +299,23 @@ class UserConverter(IDConverter[disnake.User]):

The lookup strategy is as follows (in order):

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

.. 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.
No longer automatically removes ``"@"`` prefix from arguments.
"""

async def convert(self, ctx: AnyContext, argument: str) -> disnake.User:
Expand Down Expand Up @@ -323,24 +344,21 @@ async def convert(self, ctx: AnyContext, argument: str) -> disnake.User:
return result._user
return result

arg = argument

# Remove the '@' character if this is the first character from the argument
if arg[0] == "@":
# Remove first character
arg = arg[1:]

# check for discriminator if it exists,
if len(arg) > 5 and arg[-5] == "#":
discrim = arg[-4:]
name = arg[:-5]
result = disnake.utils.find(
lambda u: u.name == name and u.discriminator == discrim, state._users.values()
)
username, sep, discriminator = argument.rpartition("#")
# n.b. there's no builtin method that only matches arabic digits, `isdecimal` is the closest one.
# it really doesn't matter much, worst case is unnecessary computations
if sep and (
discriminator == "0" or (len(discriminator) == 4 and discriminator.isdecimal())
):
# legacy behavior
result = _utils_get(state._users.values(), name=username, discriminator=discriminator)
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 == argument or u.name == argument,
state._users.values(),
)

if result is None:
raise UserNotFound(argument)
Expand Down Expand Up @@ -1000,7 +1018,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 +1049,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
51 changes: 25 additions & 26 deletions disnake/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,47 +1117,46 @@ 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 username.

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
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
# discriminator lookup.
potential_discriminator = name[-4:]

# do the actual lookup and return if found
# if it isn't found then we'll do a full name lookup below.
result = utils.get(members, name=name[:-5], discriminator=potential_discriminator)
username, sep, discriminator = name.rpartition("#")
if sep and (
discriminator == "0" or (len(discriminator) == 4 and discriminator.isdecimal())
):
# legacy behavior
result = utils.get(self._members.values(), name=username, discriminator=discriminator)
if result is not None:
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)
return utils.find(pred, self._members.values())

def _create_channel(
self,
Expand Down Expand Up @@ -4124,7 +4123,7 @@ async def query_members(
) -> List[Member]:
"""|coro|

Request members that belong to this guild whose username starts with
Request members that belong to this guild whose name starts with
the query given.

This is a websocket operation and can be slow.
Expand All @@ -4136,7 +4135,7 @@ async def query_members(
Parameters
----------
query: Optional[:class:`str`]
The string that the username's start with.
The string that the names start with.
limit: :class:`int`
The maximum number of members to send back. This must be
a number between 5 and 100.
Expand Down Expand Up @@ -4199,7 +4198,7 @@ async def search_members(
):
"""|coro|

Retrieves members that belong to this guild whose username or nickname starts with
Retrieves members that belong to this guild whose name starts with
the query given.

Note that unlike :func:`query_members`, this is not a websocket operation, but an HTTP operation.
Expand All @@ -4211,7 +4210,7 @@ async def search_members(
Parameters
----------
query: :class:`str`
The string that the usernames or nicknames start with.
The string that the names start with.
limit: :class:`int`
The maximum number of members to send back. This must be
a number between 1 and 1000.
Expand Down
31 changes: 23 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).

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,19 @@ def status(self, value: Status) -> None:

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

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

.. note::
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 `help article <https://dis.gd/app-usernames>`__ for details.
"""
return self._user.discriminator

@property
Expand Down Expand Up @@ -566,11 +578,14 @@ 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.

.. versionchanged:: 2.9
Added :attr:`.global_name`.
"""
return self.nick or self.name
return self.nick or self.global_name or self.name

@property
def display_avatar(self) -> Asset:
Expand Down
Loading