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: implement avatar decorations #889

Merged
merged 38 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e9bc233
feat: implement avatar decorations
Victorsitou Dec 8, 2022
eca32a8
feat: support animated avatar decorations
Victorsitou Dec 8, 2022
fb7d442
docs: add changelog entry
Victorsitou Dec 8, 2022
3931f10
docs: note about avatar decoration
Victorsitou Dec 8, 2022
f911fa8
misc: remove format (png is always used)
Victorsitou Dec 8, 2022
5d4e89d
misc: add avatar_decoration to Member type_checking
Victorsitou Dec 8, 2022
970ad6d
docs: fix docs
Victorsitou Dec 8, 2022
8f13660
types: add avatar_decoration type & add missing types
Victorsitou Dec 8, 2022
cf9330a
lint: fix lint error
Victorsitou Dec 8, 2022
4d26a15
Merge branch 'master' into feat/avatar-decorations
Victorsitou Dec 16, 2022
c2e5833
misc: remove unnecessary header
Victorsitou Dec 18, 2022
8cc1423
Merge branch 'feat/avatar-decorations' of https://github.com/Victorsi…
Victorsitou Dec 18, 2022
9de76a1
Merge branch 'master' into feat/avatar-decorations
Victorsitou Dec 22, 2022
adcabe0
docs: remove note
Victorsitou Dec 24, 2022
dcc3dd4
docs: add note about animated png
Victorsitou Dec 24, 2022
4460877
Merge branch 'feat/avatar-decorations' of https://github.com/Victorsi…
Victorsitou Dec 24, 2022
2304d38
fix: return avatar decoration when member/user cache is disabled
Victorsitou Dec 24, 2022
149b884
docs: reword changelog
Victorsitou Dec 27, 2022
49455f6
docs: exclude `avatar_decoration` in WidgetMember
Victorsitou Dec 27, 2022
7681d1e
Merge branch 'feat/avatar-decorations' of https://github.com/Victorsi…
Victorsitou Dec 27, 2022
f0e7a7d
fix: update `_avatar_decoration` in GUILD_MEMBER_UPDATE
Victorsitou Dec 27, 2022
3f14388
Merge branch 'master' into feat/avatar-decorations
Victorsitou Jan 12, 2023
6f684dc
typing: improve
Victorsitou Jan 12, 2023
b187544
Merge branch 'master' into feat/avatar-decorations
Victorsitou Feb 22, 2023
ae5f2a8
Merge remote-tracking branch 'upstream/master' into feat/avatar-decor…
Victorsitou Jul 14, 2023
ace1289
fix: remove no format
Victorsitou Jul 14, 2023
74f7ad0
docs: exclude `avatar_decoration` in WidgetMember
Victorsitou Jul 15, 2023
91938d6
Merge remote-tracking branch 'upstream/master' into feat/avatar-decor…
Victorsitou Jul 15, 2023
2d4e3cf
Merge remote-tracking branch 'upstream/master' into feat/avatar-decor…
Victorsitou Mar 20, 2024
5998e62
feat: update to api changes
Victorsitou Mar 20, 2024
3156d92
docs: it's now always available (?)
Victorsitou Mar 20, 2024
7b982d7
docs: update version
Victorsitou Mar 20, 2024
80cad8d
Merge remote-tracking branch 'upstream/master' into feat/avatar-decor…
Victorsitou May 20, 2024
8f27216
fix: update avatar decoration asset url
Victorsitou May 20, 2024
fc463a8
feat: implement guild-specific avatar decoration
Victorsitou May 20, 2024
86d81e1
feat: update avatar decoration data on member update
Victorsitou May 20, 2024
80a8f0d
feat: split into display_avatar_decoration and guild_avatar_decoration
Victorsitou May 21, 2024
5d3aa8b
type(Member): avatar_decoration_data is optional
Victorsitou May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/889.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add support for avatar decorations using:
- :attr:`User.avatar_decoration`
- :attr:`Member.display_avatar_decoration`
- :attr:`Member.guild_avatar_decoration`
10 changes: 10 additions & 0 deletions disnake/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
69 changes: 66 additions & 3 deletions disnake/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -274,6 +274,7 @@ class Member(disnake.abc.Messageable, _UserTag):
"_avatar",
"_communication_disabled_until",
"_flags",
"_avatar_decoration_data",
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -452,18 +457,33 @@ 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"],
user["avatar"],
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

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion disnake/types/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion disnake/types/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
10 changes: 9 additions & 1 deletion disnake/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
from .snowflake import Snowflake


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


class PartialUser(TypedDict):
id: Snowflake
username: str
Expand All @@ -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]
38 changes: 34 additions & 4 deletions disnake/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = (
Expand 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",
)
Expand All @@ -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__(
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
shiftinv marked this conversation as resolved.
Show resolved Hide resolved

.. 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.
Expand Down
2 changes: 1 addition & 1 deletion docs/api/widgets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------
Expand Down
Loading