From 26162e1c973cc5c4ca7f66a8178116e3fdf80f8b Mon Sep 17 00:00:00 2001 From: dolfies Date: Tue, 2 Jan 2024 01:54:12 -0500 Subject: [PATCH] Parse new avatar decoration format --- discord/abc.py | 8 ++++++++ discord/asset.py | 8 ++------ discord/member.py | 1 + discord/types/user.py | 9 ++++++++- discord/user.py | 43 ++++++++++++++++++++++++++++++++++++------- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 2b24c74f9..962e0a038 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -513,6 +513,14 @@ class User(Snowflake, Protocol): """ raise NotImplementedError + @property + def avatar_decoration_sku_id(self) -> Optional[int]: + """Optional[:class:`int`]: Returns the SKU ID of the user's avatar decoration, if present. + + .. versionadded:: 2.1 + """ + raise NotImplementedError + @property def default_avatar(self) -> Asset: """:class:`~discord.Asset`: Returns the default avatar for a given user.""" diff --git a/discord/asset.py b/discord/asset.py index 06f322830..1992d9ec9 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -239,12 +239,8 @@ class Asset(AssetMixin): ) @classmethod - def _from_avatar_decoration(cls, state: _State, user_id: int, decoration: str) -> Self: - # Avatar decoration presets are not available through the regular CDN endpoint - if decoration.startswith(('v1_', 'v2_')): - url = f'{cls.BASE}/avatar-decoration-presets/{decoration}.png?size=256&passthrough=true' - else: - url = f'{cls.BASE}/avatar-decorations/{user_id}/{decoration}.png?size=256&passthrough=true' + def _from_avatar_decoration(cls, state: _State, decoration: str) -> Self: + url = f'{cls.BASE}/avatar-decoration-presets/{decoration}.png?size=256&passthrough=true' return cls(state, url=url, key=decoration, animated=False, passthrough=True) @classmethod diff --git a/discord/member.py b/discord/member.py index f9e5b00ab..07082de55 100644 --- a/discord/member.py +++ b/discord/member.py @@ -289,6 +289,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag): default_avatar: Asset avatar: Optional[Asset] avatar_decoration: Optional[Asset] + avatar_decoration_sku_id: Optional[int] note: Note relationship: Optional[Relationship] is_friend: Callable[[], bool] diff --git a/discord/types/user.py b/discord/types/user.py index f1845e2d4..b692d659d 100644 --- a/discord/types/user.py +++ b/discord/types/user.py @@ -22,6 +22,8 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations + from typing import Any, Dict, List, Literal, Optional, TypedDict from typing_extensions import NotRequired @@ -34,7 +36,7 @@ class PartialUser(TypedDict): username: str discriminator: str avatar: Optional[str] - avatar_decoration: NotRequired[Optional[str]] + avatar_decoration_data: NotRequired[Optional[UserAvatarDecorationData]] public_flags: NotRequired[int] bot: NotRequired[bool] system: NotRequired[bool] @@ -90,6 +92,11 @@ class User(APIUser, total=False): nsfw_allowed: Optional[bool] +class UserAvatarDecorationData(TypedDict): + asset: str + sku_id: NotRequired[Snowflake] + + class PomeloAttempt(TypedDict): taken: bool diff --git a/discord/user.py b/discord/user.py index dd244f32d..3efe7887c 100644 --- a/discord/user.py +++ b/discord/user.py @@ -40,7 +40,7 @@ from .enums import ( from .errors import ClientException, NotFound from .flags import PublicUserFlags, PrivateUserFlags, PremiumUsageFlags, PurchasedFlags from .relationship import Relationship -from .utils import _bytes_to_base64_data, cached_slot_property, copy_doc, snowflake_time, MISSING +from .utils import _bytes_to_base64_data, _get_as_snowflake, cached_slot_property, copy_doc, snowflake_time, MISSING from .voice_client import VoiceClient if TYPE_CHECKING: @@ -61,6 +61,7 @@ if TYPE_CHECKING: APIUser as APIUserPayload, PartialUser as PartialUserPayload, User as UserPayload, + UserAvatarDecorationData, ) from .types.snowflake import Snowflake @@ -243,6 +244,7 @@ class BaseUser(_UserTag): 'global_name', '_avatar', '_avatar_decoration', + '_avatar_decoration_sku_id', '_banner', '_accent_colour', 'bot', @@ -262,6 +264,7 @@ class BaseUser(_UserTag): _state: ConnectionState _avatar: Optional[str] _avatar_decoration: Optional[str] + _avatar_decoration_sku_id: Optional[Snowflake] _banner: Optional[str] _accent_colour: Optional[int] _public_flags: int @@ -296,13 +299,16 @@ class BaseUser(_UserTag): 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) self._accent_colour = data.get('accent_color', None) self._public_flags = data.get('public_flags', 0) self.bot = data.get('bot', False) self.system = data.get('system', False) + decoration_data = data.get('avatar_decoration_data') + self._avatar_decoration = decoration_data.get('asset') if decoration_data else None + self._avatar_decoration_sku_id = _get_as_snowflake(decoration_data, 'sku_id') if decoration_data else None + @classmethod def _copy(cls, user: Self) -> Self: self = cls.__new__(cls) # bypass __init__ @@ -313,6 +319,7 @@ class BaseUser(_UserTag): self.global_name = user.global_name self._avatar = user._avatar self._avatar_decoration = user._avatar_decoration + self._avatar_decoration_sku_id = user._avatar_decoration_sku_id self._banner = user._banner self._accent_colour = user._accent_colour self._public_flags = user._public_flags @@ -323,11 +330,17 @@ class BaseUser(_UserTag): return self def _to_minimal_user_json(self) -> APIUserPayload: + decoration: Optional[UserAvatarDecorationData] = None + if self._avatar_decoration is not None: + decoration = {'asset': self._avatar_decoration} + if self._avatar_decoration_sku_id is not None: + decoration['sku_id'] = self._avatar_decoration_sku_id + user: APIUserPayload = { 'username': self.name, 'id': self.id, 'avatar': self._avatar, - 'avatar_decoration': self._avatar_decoration, + 'avatar_decoration_data': decoration, 'discriminator': self.discriminator, 'global_name': self.global_name, 'bot': self.bot, @@ -388,9 +401,19 @@ class BaseUser(_UserTag): .. versionadded:: 2.0 """ if self._avatar_decoration is not None: - return Asset._from_avatar_decoration(self._state, self.id, self._avatar_decoration) + return Asset._from_avatar_decoration(self._state, self._avatar_decoration) return None + @property + def avatar_decoration_sku_id(self) -> Optional[Snowflake]: + """Optional[:class:`int`]: Returns the avatar decoration's SKU ID. + + If the user does not have a preset avatar decoration, ``None`` is returned. + + .. versionadded:: 2.1 + """ + return self._avatar_decoration_sku_id + @property def banner(self) -> Optional[Asset]: """Optional[:class:`Asset`]: Returns the user's banner asset, if available. @@ -1045,14 +1068,20 @@ class User(BaseUser, discord.abc.Connectable, discord.abc.Messageable): if len(user) == 0 or len(user) <= 1: # Done because of typing return - original = (self.name, self._avatar, self.discriminator, self._public_flags, self._avatar_decoration) - # These keys seem to always be available + original = ( + self.name, + self._avatar, + self.discriminator, + self._public_flags, + self._avatar_decoration, + self.global_name, + ) modified = ( user['username'], user.get('avatar'), user['discriminator'], user.get('public_flags', 0), - user.get('avatar_decoration'), + (user.get('avatar_decoration_data') or {}).get('asset'), user.get('global_name'), ) if original != modified: