diff --git a/discord/abc.py b/discord/abc.py index 4fd92e121..32d843f63 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -219,7 +219,9 @@ class User(Snowflake, Protocol): name: :class:`str` The user's username. discriminator: :class:`str` - The user's discriminator. + The user's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The user's global nickname. bot: :class:`bool` If the user is a bot account. system: :class:`bool` @@ -228,6 +230,7 @@ class User(Snowflake, Protocol): name: str discriminator: str + global_name: Optional[str] bot: bool system: bool @@ -248,7 +251,7 @@ class User(Snowflake, Protocol): @property def default_avatar(self) -> Asset: - """:class:`~discord.Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator.""" + """:class:`~discord.Asset`: Returns the default avatar for a given user.""" raise NotImplementedError @property diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index e5be9d47f..0e4812226 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -186,9 +186,9 @@ class MemberConverter(IDConverter[discord.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 guild nickname + 4. Lookup by global name + 5. Lookup by user name .. versionchanged:: 1.5 Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument` @@ -196,17 +196,15 @@ class MemberConverter(IDConverter[discord.Member]): .. 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.3 + This converter lookup strategy has changed due to the removal of discriminators. """ async def query_member_named(self, guild: discord.Guild, argument: str) -> Optional[discord.Member]: cache = guild._state.member_cache_flags.joined - if len(argument) > 5 and argument[-5] == '#': - username, _, discriminator = argument.rpartition('#') - members = await guild.query_members(username, limit=100, cache=cache) - return discord.utils.get(members, name=username, discriminator=discriminator) - else: - members = await guild.query_members(argument, limit=100, cache=cache) - return discord.utils.find(lambda m: m.name == argument or m.nick == argument, members) + members = await guild.query_members(argument, limit=100, cache=cache) + return discord.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: _Bot, guild: discord.Guild, user_id: int) -> Optional[discord.Member]: ws = bot._get_websocket(shard_id=guild.shard_id) @@ -273,8 +271,8 @@ class UserConverter(IDConverter[discord.User]): 1. Lookup by ID. 2. Lookup by mention. - 3. Lookup by name#discrim - 4. Lookup by name + 3. Lookup by global name + 4. Lookup by user name .. versionchanged:: 1.5 Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument` @@ -282,6 +280,9 @@ class UserConverter(IDConverter[discord.User]): .. 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.3 + This converter lookup strategy has changed due to the removal of discriminators. """ async def convert(self, ctx: Context[BotT], argument: str) -> discord.User: @@ -307,16 +308,7 @@ class UserConverter(IDConverter[discord.User]): # 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] - predicate = lambda u: u.name == name and u.discriminator == discrim - result = discord.utils.find(predicate, state._users.values()) - if result is not None: - return result - - predicate = lambda u: u.name == arg + predicate = lambda u: u.global_name == arg or u.name == arg result = discord.utils.find(predicate, state._users.values()) if result is None: diff --git a/discord/flags.py b/discord/flags.py index b93a643e5..0fb60084d 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -816,6 +816,7 @@ class Intents(BaseFlags): - :attr:`User.name` - :attr:`User.avatar` - :attr:`User.discriminator` + - :attr:`User.global_name` For more information go to the :ref:`member intent documentation `. diff --git a/discord/guild.py b/discord/guild.py index b79f3bb21..31649376d 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1075,26 +1075,20 @@ class Guild(Hashable): 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. - - 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. - If no member is found, ``None`` is returned. .. versionchanged:: 2.0 ``name`` parameter is now positional-only. + .. versionchanged:: 2.3 + + ``discriminator`` is no longer used in lookup, due to being removed on Discord. + Parameters ----------- name: :class:`str` - The name of the member to lookup with an optional discriminator. + The name of the member to lookup. Returns -------- @@ -1103,22 +1097,10 @@ class Guild(Hashable): 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) - 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) diff --git a/discord/member.py b/discord/member.py index ee303f5bc..dad54deb0 100644 --- a/discord/member.py +++ b/discord/member.py @@ -274,7 +274,7 @@ class Member(discord.abc.Messageable, _UserTag): .. describe:: str(x) - Returns the member's name with the discriminator. + Returns the member's name with a ``@``. Attributes ---------- @@ -293,7 +293,7 @@ class Member(discord.abc.Messageable, _UserTag): guild: :class:`Guild` The guild that the member belongs to. nick: Optional[:class:`str`] - The guild specific nickname of the user. + The guild specific nickname of the user. Takes precedence over the global name. pending: :class:`bool` Whether the member is pending member verification. @@ -329,6 +329,7 @@ class Member(discord.abc.Messageable, _UserTag): name: str id: int discriminator: str + global_name: Optional[str] bot: bool system: bool created_at: datetime.datetime @@ -368,7 +369,7 @@ class Member(discord.abc.Messageable, _UserTag): def __repr__(self) -> str: return ( - f'' ) @@ -463,12 +464,12 @@ class Member(discord.abc.Messageable, _UserTag): 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.global_name, u._public_flags) # These keys seem to always be available - modified = (user['username'], user['avatar'], user['discriminator'], user.get('public_flags', 0)) + modified = (user['username'], user['avatar'], 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.global_name, u._public_flags = modified # Signal to dispatch on_user_update return to_return, u @@ -581,11 +582,11 @@ class Member(discord.abc.Messageable, _UserTag): 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 + For regular users this is just their global name or their username, + but if they have a guild specific nickname then that is returned instead. """ - return self.nick or self.name + return self.nick or self.global_name or self.name @property def display_avatar(self) -> Asset: diff --git a/discord/state.py b/discord/state.py index f2f2a0a83..c13c93b7a 100644 --- a/discord/state.py +++ b/discord/state.py @@ -356,6 +356,7 @@ class ConnectionState(Generic[ClientT]): return self._users[user_id] except KeyError: user = User(state=self, data=data) + # TODO: with the removal of discrims this becomes a bit annoying if user.discriminator != '0000': self._users[user_id] = user return user diff --git a/discord/team.py b/discord/team.py index eef2c2d81..6539e07e2 100644 --- a/discord/team.py +++ b/discord/team.py @@ -108,7 +108,7 @@ class TeamMember(BaseUser): .. describe:: str(x) - Returns the team member's name with discriminator. + Returns the team member's name with a ``@``. .. versionadded:: 1.3 @@ -119,7 +119,11 @@ 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. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The user's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.3 bot: :class:`bool` Specifies if the user is a bot account. team: :class:`Team` @@ -139,5 +143,5 @@ class TeamMember(BaseUser): 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'global_name={self.global_name!r} membership_state={self.membership_state!r}>' ) diff --git a/discord/types/user.py b/discord/types/user.py index fba5aef5b..0a17a5459 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -31,6 +31,7 @@ class PartialUser(TypedDict): username: str discriminator: str avatar: Optional[str] + global_name: Optional[str] PremiumType = Literal[0, 1, 2] @@ -40,7 +41,7 @@ class User(PartialUser, total=False): bot: bool system: bool mfa_enabled: bool - local: str + locale: str verified: bool email: Optional[str] flags: int diff --git a/discord/user.py b/discord/user.py index 1b899883a..b58afaa87 100644 --- a/discord/user.py +++ b/discord/user.py @@ -65,6 +65,7 @@ class BaseUser(_UserTag): 'name', 'id', 'discriminator', + 'global_name', '_avatar', '_banner', '_accent_colour', @@ -78,6 +79,7 @@ class BaseUser(_UserTag): name: str id: int discriminator: str + global_name: Optional[str] bot: bool system: bool _state: ConnectionState @@ -92,12 +94,12 @@ class BaseUser(_UserTag): def __repr__(self) -> str: return ( - f"" ) def __str__(self) -> str: - return f'{self.name}#{self.discriminator}' + return f'@{self.name}' def __eq__(self, other: object) -> bool: return isinstance(other, _UserTag) and other.id == self.id @@ -112,6 +114,7 @@ class BaseUser(_UserTag): self.name = data['username'] self.id = int(data['id']) self.discriminator = data['discriminator'] + self.global_name = data.get('global_name') self._avatar = data['avatar'] self._banner = data.get('banner', None) self._accent_colour = data.get('accent_color', None) @@ -126,6 +129,7 @@ class BaseUser(_UserTag): self.name = user.name self.id = user.id self.discriminator = user.discriminator + self.global_name = user.global_name self._avatar = user._avatar self._banner = user._banner self._accent_colour = user._accent_colour @@ -141,6 +145,7 @@ class BaseUser(_UserTag): 'id': self.id, 'avatar': self._avatar, 'discriminator': self.discriminator, + 'global_name': self.global_name, 'bot': self.bot, } @@ -162,8 +167,13 @@ class BaseUser(_UserTag): @property def default_avatar(self) -> Asset: - """:class:`Asset`: Returns the default avatar for a given user. This is calculated by the user's discriminator.""" - return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar)) + """:class:`Asset`: Returns the default avatar for a given user.""" + if self.discriminator == '0': + avatar_id = self.id % len(DefaultAvatar) + else: + avatar_id = int(self.discriminator) % len(DefaultAvatar) + + return Asset._from_default_avatar(self._state, avatar_id) @property def display_avatar(self) -> Asset: @@ -260,10 +270,12 @@ class BaseUser(_UserTag): 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 + For regular users this is just their global name or their username, + but if they have a guild specific nickname then that is returned instead. """ + if self.global_name: + return self.global_name return self.name def mentioned_in(self, message: Message) -> bool: @@ -305,7 +317,7 @@ class ClientUser(BaseUser): .. describe:: str(x) - Returns the user's name with discriminator. + Returns the user's name with a ``@``. Attributes ----------- @@ -314,7 +326,11 @@ class ClientUser(BaseUser): id: :class:`int` The user's unique ID. discriminator: :class:`str` - The user's discriminator. This is given when the username has conflicts. + The user's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The user's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.3 bot: :class:`bool` Specifies if the user is a bot account. system: :class:`bool` @@ -343,7 +359,7 @@ class ClientUser(BaseUser): def __repr__(self) -> str: return ( - f'' ) @@ -441,7 +457,7 @@ class User(BaseUser, discord.abc.Messageable): .. describe:: str(x) - Returns the user's name with discriminator. + Returns the user's name with a ``@``. Attributes ----------- @@ -450,7 +466,11 @@ class User(BaseUser, discord.abc.Messageable): id: :class:`int` The user's unique ID. discriminator: :class:`str` - The user's discriminator. This is given when the username has conflicts. + The user's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The user's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.3 bot: :class:`bool` Specifies if the user is a bot account. system: :class:`bool` @@ -460,7 +480,7 @@ class User(BaseUser, discord.abc.Messageable): __slots__ = ('__weakref__',) def __repr__(self) -> str: - return f'' + return f'' async def _get_channel(self) -> DMChannel: ch = await self.create_dm() diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index c574914a6..f0a649ff8 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -1301,7 +1301,13 @@ class Webhook(BaseWebhook): 'name': name, 'channel_id': channel.id, 'guild_id': channel.guild.id, - 'user': {'username': user.name, 'discriminator': user.discriminator, 'id': user.id, 'avatar': user._avatar}, + 'user': { + 'username': user.name, + 'discriminator': user.discriminator, + 'global_name': user.global_name, + 'id': user.id, + 'avatar': user._avatar, + }, } state = channel._state diff --git a/discord/widget.py b/discord/widget.py index 2a7c17a21..b2289c2e8 100644 --- a/discord/widget.py +++ b/discord/widget.py @@ -121,7 +121,7 @@ class WidgetMember(BaseUser): .. describe:: str(x) - Returns the widget member's ``name#discriminator``. + Returns the widget member's name with a ``@``. Attributes ----------- @@ -130,13 +130,17 @@ class WidgetMember(BaseUser): name: :class:`str` The member's username. discriminator: :class:`str` - The member's discriminator. + The member's discriminator. This is a legacy concept that is no longer used. + global_name: Optional[:class:`str`] + The member's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.3 bot: :class:`bool` Whether the member is a bot. status: :class:`Status` The member's status. nick: Optional[:class:`str`] - The member's nickname. + The member's guild-specific nickname. Takes precedence over the global name. avatar: Optional[:class:`str`] The member's avatar hash. activity: Optional[Union[:class:`BaseActivity`, :class:`Spotify`]] @@ -191,9 +195,7 @@ class WidgetMember(BaseUser): self.connected_channel: Optional[WidgetChannel] = connected_channel def __repr__(self) -> str: - return ( - f"" - ) + return f"" @property def display_name(self) -> str: