Skip to content

Commit cbff6dd

Browse files
authored
Add support for user collectibles
1 parent 69f06c9 commit cbff6dd

File tree

8 files changed

+256
-13
lines changed

8 files changed

+256
-13
lines changed

discord/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
from .presences import *
7676
from .primary_guild import *
7777
from .onboarding import *
78+
from .collectible import *
7879

7980

8081
class VersionInfo(NamedTuple):

discord/asset.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,16 @@ def _from_primary_guild(cls, state: _State, guild_id: int, icon_hash: str) -> Se
355355
animated=False,
356356
)
357357

358+
@classmethod
359+
def _from_user_collectible(cls, state: _State, asset: str, animated: bool = False) -> Self:
360+
name = 'static.png' if not animated else 'asset.webm'
361+
return cls(
362+
state,
363+
url=f'{cls.BASE}/assets/collectibles/{asset}{name}',
364+
key=asset,
365+
animated=animated,
366+
)
367+
358368
def __str__(self) -> str:
359369
return self._url
360370

discord/collectible.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-present Rapptz
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a
7+
copy of this software and associated documentation files (the "Software"),
8+
to deal in the Software without restriction, including without limitation
9+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
10+
and/or sell copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22+
DEALINGS IN THE SOFTWARE.
23+
"""
24+
25+
from __future__ import annotations
26+
27+
from typing import Optional, TYPE_CHECKING
28+
29+
30+
from .asset import Asset
31+
from .enums import NameplatePalette, CollectibleType, try_enum
32+
from .utils import parse_time
33+
34+
35+
if TYPE_CHECKING:
36+
from datetime import datetime
37+
38+
from .state import ConnectionState
39+
from .types.user import (
40+
Collectible as CollectiblePayload,
41+
)
42+
43+
44+
__all__ = ('Collectible',)
45+
46+
47+
class Collectible:
48+
"""Represents a user's collectible.
49+
50+
.. versionadded:: 2.7
51+
52+
Attributes
53+
----------
54+
label: :class:`str`
55+
The label of the collectible.
56+
palette: Optional[:class:`NameplatePalette`]
57+
The palette of the collectible.
58+
This is only available if ``type`` is
59+
:class:`CollectibleType.nameplate`.
60+
sku_id: :class:`int`
61+
The SKU ID of the collectible.
62+
type: :class:`CollectibleType`
63+
The type of the collectible.
64+
expires_at: Optional[:class:`datetime.datetime`]
65+
The expiration date of the collectible. If applicable.
66+
"""
67+
68+
__slots__ = (
69+
'type',
70+
'sku_id',
71+
'label',
72+
'expires_at',
73+
'palette',
74+
'_state',
75+
'_asset',
76+
)
77+
78+
def __init__(self, *, state: ConnectionState, type: str, data: CollectiblePayload) -> None:
79+
self._state: ConnectionState = state
80+
self.type: CollectibleType = try_enum(CollectibleType, type)
81+
self._asset: str = data['asset']
82+
self.sku_id: int = int(data['sku_id'])
83+
self.label: str = data['label']
84+
self.expires_at: Optional[datetime] = parse_time(data.get('expires_at'))
85+
86+
# nameplate
87+
self.palette: Optional[NameplatePalette]
88+
try:
89+
self.palette = try_enum(NameplatePalette, data['palette']) # type: ignore
90+
except KeyError:
91+
self.palette = None
92+
93+
@property
94+
def static(self) -> Asset:
95+
""":class:`Asset`: The static asset of the collectible."""
96+
return Asset._from_user_collectible(self._state, self._asset)
97+
98+
@property
99+
def animated(self) -> Asset:
100+
""":class:`Asset`: The animated asset of the collectible."""
101+
return Asset._from_user_collectible(self._state, self._asset, animated=True)
102+
103+
def __repr__(self) -> str:
104+
attrs = ['sku_id']
105+
if self.palette:
106+
attrs.append('palette')
107+
108+
joined_attrs = ' '.join(f'{attr}={getattr(self, attr)!r}' for attr in attrs)
109+
return f'<{self.type.name.title()} {joined_attrs}>'

discord/enums.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
'OnboardingMode',
8484
'SeparatorSpacing',
8585
'MediaItemLoadingState',
86+
'CollectibleType',
87+
'NameplatePalette',
8688
)
8789

8890

@@ -968,6 +970,24 @@ class MediaItemLoadingState(Enum):
968970
not_found = 3
969971

970972

973+
class CollectibleType(Enum):
974+
nameplate = 'nameplate'
975+
976+
977+
class NameplatePalette(Enum):
978+
crimson = 'crimson'
979+
berry = 'berry'
980+
sky = 'sky'
981+
teal = 'teal'
982+
forest = 'forest'
983+
bubble_gum = 'bubble_gum'
984+
violet = 'violet'
985+
cobalt = 'cobalt'
986+
clover = 'clover'
987+
lemon = 'lemon'
988+
white = 'white'
989+
990+
971991
def create_unknown_value(cls: Type[E], val: Any) -> E:
972992
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
973993
name = f'unknown_{val}'

discord/member.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
VoiceState as VoiceStatePayload,
7676
)
7777
from .primary_guild import PrimaryGuild
78+
from .collectible import Collectible
7879

7980
VocalGuildChannel = Union[VoiceChannel, StageChannel]
8081

@@ -311,6 +312,7 @@ class Member(discord.abc.Messageable, _UserTag):
311312
avatar_decoration: Optional[Asset]
312313
avatar_decoration_sku_id: Optional[int]
313314
primary_guild: PrimaryGuild
315+
collectibles: List[Collectible]
314316

315317
def __init__(self, *, data: MemberWithUserPayload, guild: Guild, state: ConnectionState):
316318
self._state: ConnectionState = state

discord/types/user.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,47 @@
2727
from typing_extensions import NotRequired
2828

2929

30-
class AvatarDecorationData(TypedDict):
30+
PremiumType = Literal[0, 1, 2, 3]
31+
NameplatePallete = Literal['crimson', 'berry', 'sky', 'teal', 'forest', 'bubble_gum', 'violet', 'cobalt', 'clover']
32+
33+
34+
class _UserSKU(TypedDict):
3135
asset: str
3236
sku_id: Snowflake
3337

3438

39+
AvatarDecorationData = _UserSKU
40+
41+
42+
class PrimaryGuild(TypedDict):
43+
identity_guild_id: Optional[int]
44+
identity_enabled: Optional[bool]
45+
tag: Optional[str]
46+
badge: Optional[str]
47+
48+
49+
class Collectible(_UserSKU):
50+
label: str
51+
expires_at: Optional[str]
52+
53+
54+
class NameplateCollectible(Collectible):
55+
palette: str
56+
57+
58+
class UserCollectibles(TypedDict):
59+
nameplate: NameplateCollectible
60+
61+
3562
class PartialUser(TypedDict):
3663
id: Snowflake
3764
username: str
3865
discriminator: str
3966
avatar: Optional[str]
4067
global_name: Optional[str]
4168
avatar_decoration_data: NotRequired[AvatarDecorationData]
42-
43-
44-
PremiumType = Literal[0, 1, 2, 3]
69+
primary_guild: NotRequired[PrimaryGuild]
70+
collectibles: NotRequired[UserCollectibles]
4571

4672

4773
class User(PartialUser, total=False):
@@ -54,10 +80,3 @@ class User(PartialUser, total=False):
5480
flags: int
5581
premium_type: PremiumType
5682
public_flags: int
57-
58-
59-
class PrimaryGuild(TypedDict):
60-
identity_guild_id: Optional[int]
61-
identity_enabled: Optional[bool]
62-
tag: Optional[str]
63-
badge: Optional[str]

discord/user.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .flags import PublicUserFlags
3434
from .utils import snowflake_time, _bytes_to_base64_data, MISSING, _get_as_snowflake
3535
from .primary_guild import PrimaryGuild
36+
from .collectible import Collectible
3637

3738
if TYPE_CHECKING:
3839
from typing_extensions import Self
@@ -49,6 +50,7 @@
4950
User as UserPayload,
5051
AvatarDecorationData,
5152
PrimaryGuild as PrimaryGuildPayload,
53+
UserCollectibles as UserCollectiblesPayload,
5254
)
5355

5456

@@ -78,6 +80,7 @@ class BaseUser(_UserTag):
7880
'_state',
7981
'_avatar_decoration_data',
8082
'_primary_guild',
83+
'_collectibles',
8184
)
8285

8386
if TYPE_CHECKING:
@@ -94,6 +97,7 @@ class BaseUser(_UserTag):
9497
_public_flags: int
9598
_avatar_decoration_data: Optional[AvatarDecorationData]
9699
_primary_guild: Optional[PrimaryGuildPayload]
100+
_collectibles: Optional[UserCollectiblesPayload]
97101

98102
def __init__(self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload]) -> None:
99103
self._state = state
@@ -132,6 +136,7 @@ def _update(self, data: Union[UserPayload, PartialUserPayload]) -> None:
132136
self.system = data.get('system', False)
133137
self._avatar_decoration_data = data.get('avatar_decoration_data')
134138
self._primary_guild = data.get('primary_guild', None)
139+
self._collectibles = data.get('collectibles', None)
135140

136141
@classmethod
137142
def _copy(cls, user: Self) -> Self:
@@ -149,6 +154,7 @@ def _copy(cls, user: Self) -> Self:
149154
self._public_flags = user._public_flags
150155
self._avatar_decoration_data = user._avatar_decoration_data
151156
self._primary_guild = user._primary_guild
157+
self._collectibles = user._collectibles
152158

153159
return self
154160

@@ -324,6 +330,16 @@ def primary_guild(self) -> PrimaryGuild:
324330
return PrimaryGuild(state=self._state, data=self._primary_guild)
325331
return PrimaryGuild._default(self._state)
326332

333+
@property
334+
def collectibles(self) -> List[Collectible]:
335+
"""List[:class:`Collectible`]: Returns a list of the user's collectibles.
336+
337+
.. versionadded:: 2.7
338+
"""
339+
if self._collectibles is None:
340+
return []
341+
return [Collectible(state=self._state, type=key, data=value) for key, value in self._collectibles.items() if value] # type: ignore
342+
327343
def mentioned_in(self, message: Message) -> bool:
328344
"""Checks if the user is mentioned in the specified message.
329345

0 commit comments

Comments
 (0)