From e6e380f90461cc6ca8db93df69b518e3465cbf5e Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 3 May 2023 23:48:46 -0400 Subject: [PATCH] Initial support for pomelo migration --- discord/abc.py | 7 ++-- discord/application.py | 13 ++++--- discord/client.py | 11 +++--- discord/ext/commands/context.py | 2 +- discord/ext/commands/converter.py | 36 ++++++++------------ discord/guild.py | 24 ++++--------- discord/http.py | 8 ++--- discord/member.py | 13 +++---- discord/state.py | 1 + discord/team.py | 15 +++++---- discord/types/user.py | 1 + discord/user.py | 56 ++++++++++++++++++++++--------- discord/widget.py | 16 +++++---- 13 files changed, 113 insertions(+), 90 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 01e311feb..85b9f95f1 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -449,7 +449,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` @@ -458,6 +460,7 @@ class User(Snowflake, Protocol): name: str discriminator: str + global_name: Optional[str] bot: bool system: bool @@ -486,7 +489,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/application.py b/discord/application.py index f056c6d37..98b062277 100644 --- a/discord/application.py +++ b/discord/application.py @@ -2542,10 +2542,13 @@ class Application(PartialApplication): # Passing a user object: await app.whitelist(user) - # Passing a stringified user: + # Passing a username + await app.whitelist('jake') + + # Passing a legacy user: await app.whitelist('Jake#0001') - # Passing a username and discriminator: + # Passing a legacy username and discriminator: await app.whitelist('Jake', '0001') Parameters @@ -2575,14 +2578,14 @@ class Application(PartialApplication): user = args[0] if isinstance(user, _UserTag): user = str(user) - username, discrim = user.split('#') + username, _, discrim = user.partition('#') elif len(args) == 2: username, discrim = args # type: ignore else: - raise TypeError(f'invite_member() takes 1 or 2 arguments but {len(args)} were given') + raise TypeError(f'whitelist() takes 1 or 2 arguments but {len(args)} were given') state = self._state - data = await state.http.invite_team_member(self.id, username, discrim) + data = await state.http.add_app_whitelist(self.id, username, discrim or 0) return ApplicationTester(self, state, data) async def create_asset( diff --git a/discord/client.py b/discord/client.py index 21ece250f..0f3d027c7 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2966,10 +2966,13 @@ class Client: # Passing a user object: await client.send_friend_request(user) - # Passing a stringified user: + # Passing a username + await client.send_friend_request('jake') + + # Passing a legacy user: await client.send_friend_request('Jake#0001') - # Passing a username and discriminator: + # Passing a legacy username and discriminator: await client.send_friend_request('Jake', '0001') Parameters @@ -2996,14 +2999,14 @@ class Client: user = args[0] if isinstance(user, _UserTag): user = str(user) - username, discrim = user.split('#') + username, _, discrim = user.partition('#') elif len(args) == 2: username, discrim = args # type: ignore else: raise TypeError(f'send_friend_request() takes 1 or 2 arguments but {len(args)} were given') state = self._connection - await state.http.send_friend_request(username, discrim) + await state.http.send_friend_request(username, discrim or 0) async def applications(self, *, with_team_applications: bool = True) -> List[Application]: """|coro| diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 96450c3ec..f5dddc09a 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -311,7 +311,7 @@ class Context(discord.abc.Messageable, Generic[BotT]): def filesize_limit(self) -> int: """:class:`int`: Returns the maximum number of bytes files can have when uploaded to this guild or DM channel associated with this context. - .. versionadded:: 2.3 + .. versionadded:: 2.1 """ return self.guild.filesize_limit if self.guild is not None else 26214400 diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 2525c9e95..1d5f0620b 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.1 + 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, guild, user_id): ws = bot.ws @@ -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.1 + 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/guild.py b/discord/guild.py index 55b2f476c..c5189af66 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1298,26 +1298,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.1 + + ``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 -------- @@ -1328,14 +1322,8 @@ class Guild(Hashable): members = self.members - if len(name) > 5 and name[-5] == '#': - potential_discriminator = name[-4:] - 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/http.py b/discord/http.py index 7f18785e1..bc41248af 100644 --- a/discord/http.py +++ b/discord/http.py @@ -2894,7 +2894,7 @@ class HTTPClient: def send_friend_request(self, username: str, discriminator: Snowflake) -> Response[None]: r = Route('POST', '/users/@me/relationships') props = choice((ContextProperties.from_add_friend, ContextProperties.from_group_dm))() # Friends, Group DM - payload = {'username': username, 'discriminator': int(discriminator)} + payload = {'username': username, 'discriminator': int(discriminator) or None} return self.request(r, json=payload, context_properties=props) @@ -3180,9 +3180,9 @@ class HTTPClient: ) def add_app_whitelist( - self, app_id: Snowflake, username: str, discriminator: str + self, app_id: Snowflake, username: str, discriminator: Snowflake ) -> Response[application.WhitelistedUser]: - payload = {'username': username, 'discriminator': discriminator} + payload = {'username': username, 'discriminator': str(discriminator) or None} return self.request( Route('POST', '/oauth2/applications/{app_id}/allowlist', app_id=app_id), @@ -3279,7 +3279,7 @@ class HTTPClient: return self.request(Route('GET', '/teams/{team_id}/members', team_id=team_id), super_properties_to_track=True) def invite_team_member(self, team_id: Snowflake, username: str, discriminator: Snowflake): - payload = {'username': username, 'discriminator': str(discriminator)} + payload = {'username': username, 'discriminator': str(discriminator) or None} return self.request( Route('POST', '/teams/{team_id}/members', team_id=team_id), json=payload, super_properties_to_track=True diff --git a/discord/member.py b/discord/member.py index 070fd2002..7f04f33a8 100644 --- a/discord/member.py +++ b/discord/member.py @@ -243,7 +243,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): .. describe:: str(x) - Returns the member's name with the discriminator. + Returns the member's name with a ``@``. Attributes ---------- @@ -253,7 +253,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _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. @@ -287,6 +287,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): name: str id: int discriminator: str + global_name: Optional[str] bot: bool system: bool created_at: datetime.datetime @@ -328,7 +329,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): def __repr__(self) -> str: return ( - f'' ) @@ -537,11 +538,11 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _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 8fb10c23d..b55db19f2 100644 --- a/discord/state.py +++ b/discord/state.py @@ -755,6 +755,7 @@ class ConnectionState: 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 428d5d9e2..75ea25c00 100644 --- a/discord/team.py +++ b/discord/team.py @@ -288,10 +288,13 @@ class Team(Hashable): # Passing a user object: await team.invite_member(user) - # Passing a stringified user: + # Passing a username + await team.invite_member('jake') + + # Passing a legacy user: await team.invite_member('Jake#0001') - # Passing a username and discriminator: + # Passing a legacy username and discriminator: await team.invite_member('Jake', '0001') Parameters @@ -323,14 +326,14 @@ class Team(Hashable): user = args[0] if isinstance(user, _UserTag): user = str(user) - username, discrim = user.split('#') + username, _, discrim = user.partition('#') elif len(args) == 2: username, discrim = args # type: ignore else: raise TypeError(f'invite_member() takes 1 or 2 arguments but {len(args)} were given') state = self._state - data = await state.http.invite_team_member(self.id, username, discrim) + data = await state.http.invite_team_member(self.id, username, discrim or 0) member = TeamMember(self, state, data) self.members.append(member) return member @@ -494,7 +497,7 @@ class TeamMember(User): .. describe:: str(x) - Returns the team member's name with discriminator. + Returns the team member's name with a ``@``. .. versionadded:: 1.3 @@ -519,7 +522,7 @@ class TeamMember(User): 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}>' ) async def remove(self) -> None: diff --git a/discord/types/user.py b/discord/types/user.py index ebcde3043..34f23ac8f 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -38,6 +38,7 @@ class PartialUser(TypedDict): public_flags: NotRequired[int] bot: NotRequired[bool] system: NotRequired[bool] + global_name: Optional[str] ConnectionType = Literal[ diff --git a/discord/user.py b/discord/user.py index 075f287af..57df55b88 100644 --- a/discord/user.py +++ b/discord/user.py @@ -58,6 +58,7 @@ if TYPE_CHECKING: from .state import ConnectionState from .types.channel import DMChannel as DMChannelPayload from .types.user import ( + APIUser as APIUserPayload, PartialUser as PartialUserPayload, User as UserPayload, ) @@ -239,6 +240,7 @@ class BaseUser(_UserTag): 'name', 'id', 'discriminator', + 'global_name', '_avatar', '_avatar_decoration', '_banner', @@ -254,6 +256,7 @@ class BaseUser(_UserTag): name: str id: int discriminator: str + global_name: Optional[str] bot: bool system: bool _state: ConnectionState @@ -269,12 +272,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 @@ -289,6 +292,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._avatar_decoration = data.get('avatar_decoration') self._banner = data.get('banner', None) @@ -304,6 +308,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._avatar_decoration = user._avatar_decoration self._banner = user._banner @@ -315,16 +320,19 @@ class BaseUser(_UserTag): return self - def _to_minimal_user_json(self) -> PartialUserPayload: - user: PartialUserPayload = { + def _to_minimal_user_json(self) -> APIUserPayload: + user: APIUserPayload = { 'username': self.name, 'id': self.id, 'avatar': self._avatar, 'avatar_decoration': self._avatar_decoration, 'discriminator': self.discriminator, + 'global_name': self.global_name, 'bot': self.bot, 'system': self.system, 'public_flags': self._public_flags, + 'banner': self._banner, + 'accent_color': self._accent_colour, } return user @@ -351,8 +359,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) % 5) + """:class:`Asset`: Returns the default avatar for a given user.""" + if self.discriminator == '0': + avatar_id = self.id % 5 + else: + avatar_id = int(self.discriminator) % 5 + + return Asset._from_default_avatar(self._state, avatar_id) @property def display_avatar(self) -> Asset: @@ -470,10 +483,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 @cached_slot_property('_cs_note') @@ -594,7 +609,7 @@ class ClientUser(BaseUser): .. describe:: str(x) - Returns the user's name with discriminator. + Returns the user's name with a ``@``. .. versionchanged:: 2.0 :attr:`Locale` is now a :class:`Locale` instead of a Optional[:class:`str`]. @@ -606,9 +621,13 @@ class ClientUser(BaseUser): id: :class:`int` The user's unique ID. discriminator: :class:`str` - The user's discriminator. + The user's discriminator. This is a legacy concept that is no longer used. bio: Optional[:class:`str`] The user's "about me" field. Could be ``None``. + global_name: Optional[:class:`str`] + The user's global nickname, taking precedence over the username in display. + + .. versionadded:: 2.1 bot: :class:`bool` Specifies if the user is a bot account. system: :class:`bool` @@ -686,8 +705,8 @@ class ClientUser(BaseUser): def __repr__(self) -> str: return ( - f'' + f'' ) def _full_update(self, data: UserPayload) -> None: @@ -928,7 +947,7 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): .. describe:: str(x) - Returns the user's name with discriminator. + Returns the user's name with a ``@``. Attributes ----------- @@ -937,7 +956,11 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): id: :class:`int` The user's unique ID. 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, taking precedence over the username in display. + + .. versionadded:: 2.1 bot: :class:`bool` Specifies if the user is a bot account. system: :class:`bool` @@ -947,7 +970,7 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): __slots__ = ('__weakref__',) def __repr__(self) -> str: - return f'<{self.__class__.__name__} id={self.id} name={self.name!r} discriminator={self.discriminator!r} bot={self.bot} system={self.system}>' + return f'' def _get_voice_client_key(self) -> Tuple[int, str]: return self._state.self_id, 'self_id' # type: ignore # self_id is always set at this point @@ -967,10 +990,11 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): user['discriminator'], user.get('public_flags', 0), user.get('avatar_decoration'), + user.get('global_name') ) if original != modified: to_return = User._copy(self) - self.name, self._avatar, self.discriminator, self._public_flags, self._avatar_decoration = modified + self.name, self._avatar, self.discriminator, self._public_flags, self._avatar_decoration, self.global_name = modified # Signal to dispatch user_update return to_return, self diff --git a/discord/widget.py b/discord/widget.py index bb6f25bc0..08e3fa0b1 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,19 @@ 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.1 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`]] The member's activity. deafened: Optional[:class:`bool`] @@ -189,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: