Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for avatar decoration #9343

Merged
merged 11 commits into from
Jan 27, 2024
16 changes: 16 additions & 0 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,22 @@ def avatar(self) -> Optional[Asset]:
"""Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar, if present."""
raise NotImplementedError

@property
def avatar_decoration(self) -> Optional[Asset]:
"""Optional[:class:`~discord.Asset`]: Returns an Asset that represents the user's avatar decoration, if present.

.. versionadded:: 2.4
"""
raise NotImplementedError

@property
def avatar_decoration_sku_id(self) -> Optional[int]:
"""Optional[:class:`int`]: Returns an integer that represents the user's avatar decoration SKU ID, if present.

.. versionadded:: 2.4
"""
raise NotImplementedError

@property
def default_avatar(self) -> Asset:
""":class:`~discord.Asset`: Returns the default avatar for a given user."""
Expand Down
9 changes: 9 additions & 0 deletions discord/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ def _from_guild_avatar(cls, state: _State, guild_id: int, member_id: int, avatar
animated=animated,
)

@classmethod
def _from_avatar_decoration(cls, state: _State, avatar_decoration: str) -> Self:
return cls(
state,
url=f'{cls.BASE}/avatar-decoration-presets/{avatar_decoration}.png?size=96',
codeofandrin marked this conversation as resolved.
Show resolved Hide resolved
key=avatar_decoration,
animated=True,
)

@classmethod
def _from_icon(cls, state: _State, object_id: int, icon_hash: str, path: str) -> Self:
return cls(
Expand Down
29 changes: 26 additions & 3 deletions discord/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
UserWithMember as UserWithMemberPayload,
)
from .types.gateway import GuildMemberUpdateEvent
from .types.user import User as UserPayload
from .types.user import User as UserPayload, AvatarDecorationData
from .abc import Snowflake
from .state import ConnectionState
from .message import Message
Expand Down Expand Up @@ -323,6 +323,7 @@ class Member(discord.abc.Messageable, _UserTag):
'_state',
'_avatar',
'_flags',
'_avatar_decoration_data',
)

if TYPE_CHECKING:
Expand All @@ -342,6 +343,8 @@ class Member(discord.abc.Messageable, _UserTag):
banner: Optional[Asset]
accent_color: Optional[Colour]
accent_colour: Optional[Colour]
avatar_decoration: Optional[Asset]
avatar_decoration_sku_id: Optional[int]

def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState):
self._state: ConnectionState = state
Expand All @@ -357,6 +360,7 @@ def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: Connecti
self._avatar: Optional[str] = data.get('avatar')
self._permissions: Optional[int]
self._flags: int = data['flags']
self._avatar_decoration_data: Optional[AvatarDecorationData] = data.get('avatar_decoration_data')
try:
self._permissions = int(data['permissions'])
except KeyError:
Expand Down Expand Up @@ -425,6 +429,7 @@ def _copy(cls, member: Self) -> Self:
self._permissions = member._permissions
self._state = member._state
self._avatar = member._avatar
self._avatar_decoration_data = member._avatar_decoration_data

# Reference will not be copied unless necessary by PRESENCE_UPDATE
# See below
Expand Down Expand Up @@ -453,6 +458,7 @@ def _update(self, data: GuildMemberUpdateEvent) -> None:
self._roles = utils.SnowflakeList(map(int, data['roles']))
self._avatar = data.get('avatar')
self._flags = data.get('flags', 0)
self._avatar_decoration_data = data.get('avatar_decoration_data')

def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Optional[Tuple[User, User]]:
self.activities = tuple(create_activity(d, self._state) for d in data['activities'])
Expand All @@ -464,18 +470,35 @@ def _presence_update(self, data: PartialPresenceUpdate, user: UserPayload) -> Op

def _update_inner_user(self, user: UserPayload) -> Optional[Tuple[User, User]]:
u = self._user
original = (u.name, u.discriminator, u._avatar, u.global_name, u._public_flags)
original = (
u.name,
u.discriminator,
u._avatar,
u.global_name,
u._public_flags,
u._avatar_decoration_data['sku_id'] if u._avatar_decoration_data is not None else None,
)

decoration_payload = user.get('avatar_decoration_data')
# These keys seem to always be available
modified = (
user['username'],
user['discriminator'],
user['avatar'],
user.get('global_name'),
user.get('public_flags', 0),
decoration_payload['sku_id'] if decoration_payload is not None else None,
)
if original != modified:
to_return = User._copy(self._user)
u.name, u.discriminator, u._avatar, u.global_name, u._public_flags = modified
u.name, u.discriminator, u._avatar, u.global_name, u._public_flags, u._avatar_decoration_data = (
user['username'],
user['discriminator'],
user['avatar'],
user.get('global_name'),
user.get('public_flags', 0),
decoration_payload,
)
# Signal to dispatch on_user_update
return to_return, u

Expand Down
3 changes: 2 additions & 1 deletion discord/types/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from .sticker import GuildSticker
from .appinfo import GatewayAppInfo, PartialAppInfo
from .guild import Guild, UnavailableGuild
from .user import User
from .user import User, AvatarDecorationData
from .threads import Thread, ThreadMember
from .scheduled_event import GuildScheduledEvent
from .audit_log import AuditLogEntry
Expand Down Expand Up @@ -224,6 +224,7 @@ class GuildMemberUpdateEvent(TypedDict):
mute: NotRequired[bool]
pending: NotRequired[bool]
communication_disabled_until: NotRequired[str]
avatar_decoration_data: NotRequired[AvatarDecorationData]


class GuildEmojisUpdateEvent(TypedDict):
Expand Down
5 changes: 4 additions & 1 deletion discord/types/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@

from typing import Optional, TypedDict
from .snowflake import SnowflakeList
from .user import User
from .user import User, AvatarDecorationData
from typing_extensions import NotRequired


class Nickname(TypedDict):
Expand All @@ -47,6 +48,7 @@ class Member(PartialMember, total=False):
pending: bool
permissions: str
communication_disabled_until: str
avatar_decoration_data: NotRequired[AvatarDecorationData]


class _OptionalMemberWithUser(PartialMember, total=False):
Expand All @@ -56,6 +58,7 @@ class _OptionalMemberWithUser(PartialMember, total=False):
pending: bool
permissions: str
communication_disabled_until: str
avatar_decoration_data: NotRequired[AvatarDecorationData]


class MemberWithUser(_OptionalMemberWithUser):
Expand Down
7 changes: 7 additions & 0 deletions discord/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@

from .snowflake import Snowflake
from typing import Literal, Optional, TypedDict
from typing_extensions import NotRequired


class AvatarDecorationData(TypedDict):
asset: str
sku_id: Snowflake


class PartialUser(TypedDict):
Expand All @@ -32,6 +38,7 @@ class PartialUser(TypedDict):
discriminator: str
avatar: Optional[str]
global_name: Optional[str]
avatar_decoration_data: NotRequired[AvatarDecorationData]


PremiumType = Literal[0, 1, 2, 3]
Expand Down
35 changes: 30 additions & 5 deletions discord/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from .colour import Colour
from .enums import DefaultAvatar
from .flags import PublicUserFlags
from .utils import snowflake_time, _bytes_to_base64_data, MISSING
from .utils import snowflake_time, _bytes_to_base64_data, MISSING, _get_as_snowflake

if TYPE_CHECKING:
from typing_extensions import Self
Expand All @@ -43,10 +43,7 @@
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 PartialUser as PartialUserPayload, User as UserPayload, AvatarDecorationData


__all__ = (
Expand All @@ -73,6 +70,7 @@ class BaseUser(_UserTag):
'system',
'_public_flags',
'_state',
'_avatar_decoration_data',
)

if TYPE_CHECKING:
Expand All @@ -87,6 +85,7 @@ class BaseUser(_UserTag):
_banner: Optional[str]
_accent_colour: Optional[int]
_public_flags: int
_avatar_decoration_data: Optional[AvatarDecorationData]

def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None:
self._state = state
Expand Down Expand Up @@ -123,6 +122,7 @@ def _update(self, data: Union[UserPayload, PartialUserPayload]) -> None:
self._public_flags = data.get('public_flags', 0)
self.bot = data.get('bot', False)
self.system = data.get('system', False)
self._avatar_decoration_data = data.get('avatar_decoration_data')

@classmethod
def _copy(cls, user: Self) -> Self:
Expand All @@ -138,6 +138,7 @@ def _copy(cls, user: Self) -> Self:
self.bot = user.bot
self._state = user._state
self._public_flags = user._public_flags
self._avatar_decoration_data = user._avatar_decoration_data

return self

Expand Down Expand Up @@ -187,6 +188,30 @@ def display_avatar(self) -> Asset:
"""
return self.avatar or self.default_avatar

@property
codeofandrin marked this conversation as resolved.
Show resolved Hide resolved
def avatar_decoration(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns an :class:`Asset` for the avatar decoration the user has.

If the user has not set an avatar decoration, ``None`` is returned.

.. versionadded:: 2.4
"""
if self._avatar_decoration_data is not None:
return Asset._from_avatar_decoration(self._state, self._avatar_decoration_data['asset'])
return None

@property
def avatar_decoration_sku_id(self) -> Optional[int]:
"""Optional[:class:`int`]: Returns the SKU ID of the avatar decoration the user has.

If the user has not set an avatar decoration, ``None`` is returned.

.. versionadded:: 2.4
"""
if self._avatar_decoration_data is not None:
return _get_as_snowflake(self._avatar_decoration_data, 'sku_id')
return None

@property
def banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the user's banner asset, if available.
Expand Down
3 changes: 2 additions & 1 deletion discord/webhook/async_.py
Original file line number Diff line number Diff line change
Expand Up @@ -1304,9 +1304,10 @@ def _as_follower(cls, data, *, channel, user) -> Self:
'user': {
'username': user.name,
'discriminator': user.discriminator,
'global_name': user.global_name,
'id': user.id,
'avatar': user._avatar,
'avatar_decoration_data': user._avatar_decoration_data,
'global_name': user.global_name,
},
}

Expand Down