From e5236b94d95fd434a7439276f17ee97e43d4145c Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Thu, 8 Dec 2022 20:21:17 +0100 Subject: [PATCH 1/4] Add support for avatar decorations --- nextcord/asset.py | 13 +++++++++++++ nextcord/member.py | 1 + nextcord/user.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/nextcord/asset.py b/nextcord/asset.py index 06c46671cd..0b53e75c42 100644 --- a/nextcord/asset.py +++ b/nextcord/asset.py @@ -220,6 +220,19 @@ def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: animated=animated, ) + @classmethod + def _from_avatar_decoration(cls, state, user_id: int, decoration: str) -> Asset: + animated = decoration.startswith("a_") + # format = "gif" if animated else "png" + # animated decorations are not supported yet? + # they're always returned as .png even if the hash starts with a_ + return cls( + state, + url=f"{cls.BASE}/avatar-decorations/{user_id}/{decoration}.png", + key=decoration, + animated=animated, + ) + @classmethod def _from_guild_avatar(cls, state, guild_id: int, member_id: int, avatar: str) -> Asset: animated = avatar.startswith("a_") diff --git a/nextcord/member.py b/nextcord/member.py index 89de9e1706..cd940505ee 100644 --- a/nextcord/member.py +++ b/nextcord/member.py @@ -260,6 +260,7 @@ class Member(abc.Messageable, _UserTag): banner: Optional[Asset] accent_color: Optional[Colour] accent_colour: Optional[Colour] + avatar_decoration: Optional[Asset] def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState): self._state: ConnectionState = state diff --git a/nextcord/user.py b/nextcord/user.py index 2dbb7030ba..b3237a21b8 100644 --- a/nextcord/user.py +++ b/nextcord/user.py @@ -44,6 +44,7 @@ class BaseUser(_UserTag): "_avatar", "_banner", "_accent_colour", + "_avatar_decoration", "bot", "system", "_public_flags", @@ -60,6 +61,7 @@ class BaseUser(_UserTag): _avatar: Optional[str] _banner: Optional[str] _accent_colour: Optional[str] + _avatar_decoration: Optional[str] _public_flags: int def __init__( @@ -93,6 +95,7 @@ def _update(self, data: Union[PartialUserPayload, UserPayload]) -> None: self._avatar = data["avatar"] self._banner = data.get("banner", None) self._accent_colour = data.get("accent_color", None) + self._avatar_decoration = data.get("avatar_decoration", None) self._public_flags = data.get("public_flags", 0) self.bot = data.get("bot", False) self.system = data.get("system", False) @@ -107,6 +110,7 @@ def _copy(cls, user: Self) -> Self: self._avatar = user._avatar self._banner = user._banner self._accent_colour = user._accent_colour + self._avatar_decoration = user._avatar_decoration self.bot = user.bot self._state = user._state self._public_flags = user._public_flags @@ -200,6 +204,20 @@ def accent_color(self) -> Optional[Colour]: """ return self.accent_colour + @property + def avatar_decoration(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the user's avatar decoration, if applicable. + + .. versionadded:: 2.4 + + .. note:: + + This information is only available via :meth:`Client.fetch_user`. + """ + if self._avatar_decoration is None: + return None + return Asset._from_avatar_decoration(self._state, self.id, self._avatar_decoration) + @property def colour(self) -> Colour: """:class:`Colour`: A property that returns a colour denoting the rendered colour From 4b096bc4d1e5cfbac924e219ee694c59bc485e2a Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Tue, 20 Dec 2022 19:10:49 +0100 Subject: [PATCH 2/4] Better comment Co-Authored-By: Emre Terzioglu <50607143+EmreTech@users.noreply.github.com> --- nextcord/asset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nextcord/asset.py b/nextcord/asset.py index 0b53e75c42..0af3a2a677 100644 --- a/nextcord/asset.py +++ b/nextcord/asset.py @@ -223,9 +223,9 @@ def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: @classmethod def _from_avatar_decoration(cls, state, user_id: int, decoration: str) -> Asset: animated = decoration.startswith("a_") - # format = "gif" if animated else "png" - # animated decorations are not supported yet? - # they're always returned as .png even if the hash starts with a_ + # some decorations are animated but they are returned as an animated png, - + # - you can't get them as a gif (like stickers) + # their hashes start with a_ return cls( state, url=f"{cls.BASE}/avatar-decorations/{user_id}/{decoration}.png", From 18fa5e182ecd8a2fdea05168101b1558b5c03e75 Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Tue, 20 Dec 2022 19:11:11 +0100 Subject: [PATCH 3/4] Add missing default size --- nextcord/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nextcord/asset.py b/nextcord/asset.py index 0af3a2a677..85959adfc1 100644 --- a/nextcord/asset.py +++ b/nextcord/asset.py @@ -228,7 +228,7 @@ def _from_avatar_decoration(cls, state, user_id: int, decoration: str) -> Asset: # their hashes start with a_ return cls( state, - url=f"{cls.BASE}/avatar-decorations/{user_id}/{decoration}.png", + url=f"{cls.BASE}/avatar-decorations/{user_id}/{decoration}.png?size=1024", key=decoration, animated=animated, ) From 31b3202a7e508efddd0b4f6275fed78b0ef6fa6e Mon Sep 17 00:00:00 2001 From: Soheab_ <33902984+Soheab@users.noreply.github.com> Date: Sun, 1 Oct 2023 19:43:09 +0200 Subject: [PATCH 4/4] Update according to new data https://github.com/discord/discord-api-docs/pull/6464 --- nextcord/asset.py | 4 +- nextcord/member.py | 3 +- nextcord/types/user.py | 6 +++ nextcord/user.py | 88 +++++++++++++++++++++++++++++++++--------- 4 files changed, 80 insertions(+), 21 deletions(-) diff --git a/nextcord/asset.py b/nextcord/asset.py index e35e47428f..4889ffaa2f 100644 --- a/nextcord/asset.py +++ b/nextcord/asset.py @@ -221,14 +221,14 @@ def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: ) @classmethod - def _from_avatar_decoration(cls, state, user_id: int, decoration: str) -> Asset: + def _from_avatar_decoration(cls, state, decoration: str) -> Asset: animated = decoration.startswith("a_") # some decorations are animated but they are returned as an animated png, - # - you can't get them as a gif (like stickers) # their hashes start with a_ return cls( state, - url=f"{cls.BASE}/avatar-decorations/{user_id}/{decoration}.png?size=1024", + url=f"{cls.BASE}/avatar-decoration-presets/{decoration}.png?size=1024", key=decoration, animated=animated, ) diff --git a/nextcord/member.py b/nextcord/member.py index a364160c5d..aaa58b0c25 100644 --- a/nextcord/member.py +++ b/nextcord/member.py @@ -42,6 +42,7 @@ ) from .types.user import User as UserPayload from .types.voice import VoiceState as VoiceStatePayload + from .user import AvatarDecoration VocalGuildChannel = Union[VoiceChannel, StageChannel] @@ -263,7 +264,7 @@ class Member(abc.Messageable, _UserTag): banner: Optional[Asset] accent_color: Optional[Colour] accent_colour: Optional[Colour] - avatar_decoration: Optional[Asset] + avatar_decoration: Optional[AvatarDecoration] def __init__( self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState diff --git a/nextcord/types/user.py b/nextcord/types/user.py index b75728fad4..3994ade9aa 100644 --- a/nextcord/types/user.py +++ b/nextcord/types/user.py @@ -5,6 +5,11 @@ from .snowflake import Snowflake +class AvatarDecorationData(TypedDict): + sku_id: str + asset: str + + class PartialUser(TypedDict): id: Snowflake username: str @@ -26,3 +31,4 @@ class User(PartialUser, total=False): premium_type: PremiumType public_flags: int global_name: Optional[str] + avatar_decoration_data: Optional[AvatarDecorationData] diff --git a/nextcord/user.py b/nextcord/user.py index 0a59fcefb9..2b5bb7597f 100644 --- a/nextcord/user.py +++ b/nextcord/user.py @@ -22,7 +22,11 @@ from .message import Attachment, Message from .state import ConnectionState from .types.channel import DMChannel as DMChannelPayload - from .types.user import PartialUser as PartialUserPayload, User as UserPayload + from .types.user import ( + AvatarDecorationData as AvatarDecorationDataPayload, + PartialUser as PartialUserPayload, + User as UserPayload, + ) __all__ = ( @@ -31,6 +35,51 @@ ) +class AvatarDecoration: + """Represents an avatar decoration. This is a cosmetic item that can be applied to a user's avatar. + + You can get this object via :meth:`User.avatar_decoration`. + + .. versionadded:: 2.7 + + Attributes + ---------- + user: :class:`.BaseUser` + The user this avatar decoration belongs to. + sku_id: :class:`str` + The sku id of the avatar decoration. + asset: :class:`Asset` + The asset of the avatar decoration. + """ + + __slots__ = ("user", "sku_id", "_asset", "_state") + + def __init__(self, *, user: BaseUser, data: AvatarDecorationDataPayload) -> None: + self._update(user, data) + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, AvatarDecoration) + and other.sku_id == self.sku_id + and other.asset == self.asset + ) + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def _update(self, user: BaseUser, data: AvatarDecorationDataPayload, /) -> None: + self.user: BaseUser = user + self.sku_id: str = data["sku_id"] + self._asset: str = data["asset"] + + @property + def asset(self) -> Asset: + return Asset._from_avatar_decoration(self.user._state, self._asset) + + class _UserTag: __slots__ = () id: int @@ -44,12 +93,12 @@ class BaseUser(_UserTag): "_avatar", "_banner", "_accent_colour", - "_avatar_decoration", "bot", "system", "_public_flags", "_state", "global_name", + "_avatar_decoration", ) if TYPE_CHECKING: @@ -63,8 +112,8 @@ class BaseUser(_UserTag): _avatar: Optional[str] _banner: Optional[str] _accent_colour: Optional[str] - _avatar_decoration: Optional[str] _public_flags: int + _avatar_decoration: Optional[AvatarDecorationDataPayload] def __init__( self, *, state: ConnectionState, data: Union[PartialUserPayload, UserPayload] @@ -98,7 +147,7 @@ def _update(self, data: Union[PartialUserPayload, UserPayload]) -> None: self._avatar = data["avatar"] self._banner = data.get("banner", None) self._accent_colour = data.get("accent_color", None) - self._avatar_decoration = data.get("avatar_decoration", None) + self._avatar_decoration = data.get("avatar_decoration_data", None) self._public_flags = data.get("public_flags", 0) self.bot = data.get("bot", False) self.system = data.get("system", False) @@ -172,6 +221,23 @@ def display_avatar(self) -> Asset: """ return self.avatar or self.default_avatar + @property + def avatar_decoration(self) -> Optional[AvatarDecoration]: + """Optional[:class:`AvatarDecoration`]: Returns the user's avatar decoration, if applicable. + + You can get the asset of the avatar decoration via :attr:`AvatarDecoration.asset`. + + .. versionadded:: 2.7 + + .. note:: + + This information is only available via :meth:`Client.fetch_user`. + """ + if self._avatar_decoration is None: + return None + + return AvatarDecoration(user=self, data=self._avatar_decoration) + @property def banner(self) -> Optional[Asset]: """Optional[:class:`Asset`]: Returns the user's banner asset, if available. @@ -216,20 +282,6 @@ def accent_color(self) -> Optional[Colour]: """ return self.accent_colour - @property - def avatar_decoration(self) -> Optional[Asset]: - """Optional[:class:`Asset`]: Returns the user's avatar decoration, if applicable. - - .. versionadded:: 2.4 - - .. note:: - - This information is only available via :meth:`Client.fetch_user`. - """ - if self._avatar_decoration is None: - return None - return Asset._from_avatar_decoration(self._state, self.id, self._avatar_decoration) - @property def colour(self) -> Colour: """:class:`Colour`: A property that returns a colour denoting the rendered colour