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

feat: avatar decorations #941

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions nextcord/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,19 @@ def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset:
animated=animated,
)

@classmethod
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-decoration-presets/{decoration}.png?size=1024",
key=decoration,
animated=animated,
Soheab marked this conversation as resolved.
Show resolved Hide resolved
)

@classmethod
def _from_guild_avatar(cls, state, guild_id: int, member_id: int, avatar: str) -> Asset:
animated = avatar.startswith("a_")
Expand Down
2 changes: 2 additions & 0 deletions nextcord/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -263,6 +264,7 @@ class Member(abc.Messageable, _UserTag):
banner: Optional[Asset]
accent_color: Optional[Colour]
accent_colour: Optional[Colour]
avatar_decoration: Optional[AvatarDecoration]

def __init__(
self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState
Expand Down
6 changes: 6 additions & 0 deletions nextcord/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from .snowflake import Snowflake


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


class PartialUser(TypedDict):
id: Snowflake
username: str
Expand All @@ -26,3 +31,4 @@ class User(PartialUser, total=False):
premium_type: PremiumType
public_flags: int
global_name: Optional[str]
avatar_decoration_data: Optional[AvatarDecorationData]
72 changes: 71 additions & 1 deletion nextcord/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = (
Expand All @@ -31,6 +35,51 @@
)


class AvatarDecoration:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to add this to api.rst.

"""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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionadded:: 2.7
.. versionadded:: 3.0


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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_state seems to be self.user._state, not self._state.


def __init__(self, *, user: BaseUser, data: AvatarDecorationDataPayload) -> None:
self._update(user, data)

def __repr__(self) -> str:
return f"<AvatarDecoration sku_id={self.sku_id!r} asset={self.asset!r}>"

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
Expand All @@ -49,6 +98,7 @@ class BaseUser(_UserTag):
"_public_flags",
"_state",
"global_name",
"_avatar_decoration",
)

if TYPE_CHECKING:
Expand All @@ -63,6 +113,7 @@ class BaseUser(_UserTag):
_banner: Optional[str]
_accent_colour: Optional[str]
_public_flags: int
_avatar_decoration: Optional[AvatarDecorationDataPayload]

def __init__(
self, *, state: ConnectionState, data: Union[PartialUserPayload, UserPayload]
Expand Down Expand Up @@ -96,6 +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_data", None)
self._public_flags = data.get("public_flags", 0)
self.bot = data.get("bot", False)
self.system = data.get("system", False)
Expand All @@ -111,6 +163,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
Expand Down Expand Up @@ -168,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionadded:: 2.7
.. versionadded:: 3.0


.. 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.
Expand Down