diff --git a/changelog/889.feature.rst b/changelog/889.feature.rst new file mode 100644 index 0000000000..c091a7831a --- /dev/null +++ b/changelog/889.feature.rst @@ -0,0 +1,4 @@ +Add support for avatar decorations using: +- :attr:`User.avatar_decoration` +- :attr:`Member.display_avatar_decoration` +- :attr:`Member.guild_avatar_decoration` diff --git a/disnake/asset.py b/disnake/asset.py index ad83b12fe2..edb0d1c7a6 100644 --- a/disnake/asset.py +++ b/disnake/asset.py @@ -317,6 +317,16 @@ def _from_guild_scheduled_event_image( animated=False, ) + @classmethod + def _from_avatar_decoration(cls, state: AnyState, avatar_decoration_asset: str) -> Self: + animated = avatar_decoration_asset.startswith("a_") + return cls( + state, + url=f"{cls.BASE}/avatar-decoration-presets/{avatar_decoration_asset}.png?size=1024", + key=avatar_decoration_asset, + animated=animated, + ) + def __str__(self) -> str: return self._url diff --git a/disnake/member.py b/disnake/member.py index 5c2d737d85..149fc97ecc 100644 --- a/disnake/member.py +++ b/disnake/member.py @@ -58,7 +58,7 @@ MemberWithUser as MemberWithUserPayload, UserWithMember as UserWithMemberPayload, ) - from .types.user import User as UserPayload + from .types.user import AvatarDecorationData as AvatarDecorationDataPayload, User as UserPayload from .types.voice import ( GuildVoiceState as GuildVoiceStatePayload, VoiceState as VoiceStatePayload, @@ -274,6 +274,7 @@ class Member(disnake.abc.Messageable, _UserTag): "_avatar", "_communication_disabled_until", "_flags", + "_avatar_decoration_data", ) if TYPE_CHECKING: @@ -342,6 +343,9 @@ def __init__( timeout_datetime = utils.parse_time(data.get("communication_disabled_until")) self._communication_disabled_until: Optional[datetime.datetime] = timeout_datetime self._flags: int = data.get("flags", 0) + self._avatar_decoration_data: Optional[AvatarDecorationDataPayload] = data.get( + "avatar_decoration_data" + ) def __str__(self) -> str: return str(self._user) @@ -436,6 +440,7 @@ def _update(self, data: GuildMemberUpdateEvent) -> None: timeout_datetime = utils.parse_time(data.get("communication_disabled_until")) self._communication_disabled_until = timeout_datetime self._flags = data.get("flags", 0) + self._avatar_decoration_data = data.get("avatar_decoration_data") def _presence_update( self, data: PresenceData, user: UserPayload @@ -452,7 +457,14 @@ 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.global_name, u._public_flags) + original = ( + u.name, + u._avatar, + u.discriminator, + u.global_name, + u._public_flags, + u._avatar_decoration_data, + ) # These keys seem to always be available modified = ( user["username"], @@ -460,10 +472,18 @@ def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]: user["discriminator"], user.get("global_name"), user.get("public_flags", 0), + user.get("avatar_decoration_data", None), ) if original != modified: to_return = User._copy(self._user) - u.name, u._avatar, u.discriminator, u.global_name, u._public_flags = modified + ( + u.name, + u._avatar, + u.discriminator, + u.global_name, + u._public_flags, + u._avatar_decoration_data, + ) = modified # Signal to dispatch on_user_update return to_return, u @@ -718,6 +738,49 @@ def flags(self) -> MemberFlags: """ return MemberFlags._from_value(self._flags) + @property + def display_avatar_decoration(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the member's display avatar decoration. + + For regular members this is just their avatar decoration, but + if they have a guild specific avatar decoration then that + is returned instead. + + .. versionadded:: 2.10 + + .. note:: + + Since Discord always sends an animated PNG for animated avatar decorations, + the following methods will not work as expected: + + - :meth:`Asset.replace` + - :meth:`Asset.with_size` + - :meth:`Asset.with_format` + - :meth:`Asset.with_static_format` + """ + return self.guild_avatar_decoration or self._user.avatar_decoration + + @property + def guild_avatar_decoration(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild avatar decoration + the member has. If unavailable, ``None`` is returned. + + .. versionadded:: 2.10 + + .. note:: + + Since Discord always sends an animated PNG for animated avatar decorations, + the following methods will not work as expected: + + - :meth:`Asset.replace` + - :meth:`Asset.with_size` + - :meth:`Asset.with_format` + - :meth:`Asset.with_static_format` + """ + if self._avatar_decoration_data is None: + return None + return Asset._from_avatar_decoration(self._state, self._avatar_decoration_data["asset"]) + @overload async def ban( self, diff --git a/disnake/types/gateway.py b/disnake/types/gateway.py index 4ae10bdb41..e2494848b7 100644 --- a/disnake/types/gateway.py +++ b/disnake/types/gateway.py @@ -24,7 +24,7 @@ from .snowflake import Snowflake, SnowflakeList from .sticker import GuildSticker from .threads import Thread, ThreadMember, ThreadMemberWithPresence, ThreadType -from .user import User +from .user import AvatarDecorationData, User from .voice import GuildVoiceState, SupportedModes @@ -441,6 +441,7 @@ class GuildMemberUpdateEvent(TypedDict): pending: NotRequired[bool] communication_disabled_until: NotRequired[Optional[str]] flags: int + avatar_decoration_data: NotRequired[Optional[AvatarDecorationData]] # https://discord.com/developers/docs/topics/gateway-events#guild-emojis-update diff --git a/disnake/types/member.py b/disnake/types/member.py index 9daeab5467..0cb38823c1 100644 --- a/disnake/types/member.py +++ b/disnake/types/member.py @@ -5,7 +5,7 @@ from typing_extensions import NotRequired from .snowflake import SnowflakeList -from .user import User +from .user import AvatarDecorationData, User class BaseMember(TypedDict): @@ -20,6 +20,7 @@ class BaseMember(TypedDict): permissions: NotRequired[str] communication_disabled_until: NotRequired[Optional[str]] flags: int + avatar_decoration_data: NotRequired[Optional[AvatarDecorationData]] class Member(BaseMember, total=False): diff --git a/disnake/types/user.py b/disnake/types/user.py index ab913f9e82..8f7495ae3d 100644 --- a/disnake/types/user.py +++ b/disnake/types/user.py @@ -7,6 +7,11 @@ from .snowflake import Snowflake +class AvatarDecorationData(TypedDict): + asset: str + sku_id: Snowflake + + class PartialUser(TypedDict): id: Snowflake username: str @@ -22,9 +27,12 @@ class User(PartialUser, total=False): bot: bool system: bool mfa_enabled: bool - local: str + banner: Optional[str] + accent_color: Optional[int] + locale: str verified: bool email: Optional[str] flags: int premium_type: PremiumType public_flags: int + avatar_decoration_data: Optional[AvatarDecorationData] diff --git a/disnake/user.py b/disnake/user.py index 92c60585fe..b4a66505bb 100644 --- a/disnake/user.py +++ b/disnake/user.py @@ -23,7 +23,11 @@ from .message import 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__ = ( @@ -43,11 +47,12 @@ class BaseUser(_UserTag): "id", "discriminator", "global_name", + "bot", + "system", "_avatar", "_banner", + "_avatar_decoration_data", "_accent_colour", - "bot", - "system", "_public_flags", "_state", ) @@ -62,7 +67,8 @@ class BaseUser(_UserTag): _state: ConnectionState _avatar: Optional[str] _banner: Optional[str] - _accent_colour: Optional[str] + _avatar_decoration_data: Optional[AvatarDecorationDataPayload] + _accent_colour: Optional[int] _public_flags: int def __init__( @@ -100,6 +106,7 @@ def _update(self, data: Union[UserPayload, PartialUserPayload]) -> None: self.global_name = data.get("global_name") self._avatar = data["avatar"] self._banner = data.get("banner", None) + self._avatar_decoration_data = data.get("avatar_decoration_data", None) self._accent_colour = data.get("accent_color", None) self._public_flags = data.get("public_flags", 0) self.bot = data.get("bot", False) @@ -115,6 +122,7 @@ def _copy(cls, user: BaseUser) -> Self: self.global_name = user.global_name self._avatar = user._avatar self._banner = user._banner + self._avatar_decoration_data = user._avatar_decoration_data self._accent_colour = user._accent_colour self.bot = user.bot self._state = user._state @@ -131,6 +139,7 @@ def _to_minimal_user_json(self) -> UserPayload: "global_name": self.global_name, "bot": self.bot, "public_flags": self._public_flags, + "avatar_decoration_data": self._avatar_decoration_data, } @property @@ -180,12 +189,33 @@ def banner(self) -> Optional[Asset]: .. versionadded:: 2.0 .. note:: + This information is only available via :meth:`Client.fetch_user`. """ if self._banner is None: return None return Asset._from_banner(self._state, self.id, self._banner) + @property + def avatar_decoration(self) -> Optional[Asset]: + """Optional[:class:`Asset`]: Returns the user's avatar decoration asset, if available. + + .. versionadded:: 2.10 + + .. note:: + + Since Discord always sends an animated PNG for animated avatar decorations, + the following methods will not work as expected: + + - :meth:`Asset.replace` + - :meth:`Asset.with_size` + - :meth:`Asset.with_format` + - :meth:`Asset.with_static_format` + """ + if self._avatar_decoration_data is None: + return None + return Asset._from_avatar_decoration(self._state, self._avatar_decoration_data["asset"]) + @property def accent_colour(self) -> Optional[Colour]: """Optional[:class:`Colour`]: Returns the user's accent colour, if applicable. diff --git a/docs/api/widgets.rst b/docs/api/widgets.rst index b0565bed04..386a91f37e 100644 --- a/docs/api/widgets.rst +++ b/docs/api/widgets.rst @@ -44,7 +44,7 @@ WidgetMember .. autoclass:: WidgetMember() :members: :inherited-members: - :exclude-members: global_name, public_flags, default_avatar, banner, accent_colour, accent_color, colour, color, mention, created_at, mentioned_in + :exclude-members: global_name, public_flags, default_avatar, banner, accent_colour, accent_color, colour, color, mention, created_at, mentioned_in, avatar_decoration Enumerations ------------