Browse Source

Add new user and profile fields

pull/10109/head
Dolfies 5 months ago
parent
commit
f88c1d37ce
  1. 10
      discord/abc.py
  2. 15
      discord/client.py
  3. 9
      discord/guild.py
  4. 2
      discord/http.py
  5. 4
      discord/member.py
  6. 42
      discord/profile.py
  7. 2
      discord/types/profile.py
  8. 1
      discord/types/user.py
  9. 31
      discord/user.py
  10. 12
      discord/utils.py

10
discord/abc.py

@ -490,6 +490,16 @@ class User(Snowflake, Protocol):
""" """
raise NotImplementedError raise NotImplementedError
@property
def avatar_decoration_expires_at(self) -> Optional[datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the avatar decoration's expiration time.
If the user does not have an expiring avatar decoration, ``None`` is returned.
.. versionadded:: 2.1
"""
raise NotImplementedError
@property @property
def default_avatar(self) -> Asset: def default_avatar(self) -> Asset:
""":class:`~discord.Asset`: Returns the default avatar for a given user.""" """:class:`~discord.Asset`: Returns the default avatar for a given user."""

15
discord/client.py

@ -2523,8 +2523,7 @@ class Client:
.. versionadded:: 2.0 .. versionadded:: 2.0
with_mutual_friends: :class:`bool` with_mutual_friends: :class:`bool`
Whether to fetch mutual friends. Whether to fetch mutual friends.
This fills in :attr:`.UserProfile.mutual_friends` and :attr:`.UserProfile.mutual_friends_count`, This fills in :attr:`.UserProfile.mutual_friends` and :attr:`.UserProfile.mutual_friends_count`.
but requires an extra API call.
.. versionadded:: 2.0 .. versionadded:: 2.0
@ -2533,7 +2532,7 @@ class Client:
NotFound NotFound
A user with this ID does not exist. A user with this ID does not exist.
Forbidden Forbidden
You do not have a mutual with this user, and and the user is not a bot. You do not have a mutual with this user, and the user is not a bot.
HTTPException HTTPException
Fetching the profile failed. Fetching the profile failed.
@ -2544,13 +2543,13 @@ class Client:
""" """
state = self._connection state = self._connection
data = await state.http.get_user_profile( data = await state.http.get_user_profile(
user_id, with_mutual_guilds=with_mutual_guilds, with_mutual_friends_count=with_mutual_friends_count user_id,
with_mutual_guilds=with_mutual_guilds,
with_mutual_friends_count=with_mutual_friends_count,
with_mutual_friends=with_mutual_friends,
) )
mutual_friends = None
if with_mutual_friends and not data['user'].get('bot', False):
mutual_friends = await state.http.get_mutual_friends(user_id)
return UserProfile(state=state, data=data, mutual_friends=mutual_friends) return UserProfile(state=state, data=data)
async def fetch_channel(self, channel_id: int, /) -> Union[GuildChannel, PrivateChannel, Thread]: async def fetch_channel(self, channel_id: int, /) -> Union[GuildChannel, PrivateChannel, Thread]:
"""|coro| """|coro|

9
discord/guild.py

@ -2702,8 +2702,7 @@ class Guild(Hashable):
This fills in :attr:`.MemberProfile.mutual_friends_count`. This fills in :attr:`.MemberProfile.mutual_friends_count`.
with_mutual_friends: :class:`bool` with_mutual_friends: :class:`bool`
Whether to fetch mutual friends. Whether to fetch mutual friends.
This fills in :attr:`.MemberProfile.mutual_friends` and :attr:`.MemberProfile.mutual_friends_count`, This fills in :attr:`.MemberProfile.mutual_friends` and :attr:`.MemberProfile.mutual_friends_count`.
but requires an extra API call.
Raises Raises
------- -------
@ -2727,16 +2726,14 @@ class Guild(Hashable):
self.id, self.id,
with_mutual_guilds=with_mutual_guilds, with_mutual_guilds=with_mutual_guilds,
with_mutual_friends_count=with_mutual_friends_count, with_mutual_friends_count=with_mutual_friends_count,
with_mutual_friends=with_mutual_friends,
) )
if 'guild_member_profile' not in data: if 'guild_member_profile' not in data:
raise InvalidData('Member is not in this guild') raise InvalidData('Member is not in this guild')
if 'guild_member' not in data: if 'guild_member' not in data:
raise InvalidData('Member has blocked you') raise InvalidData('Member has blocked you')
mutual_friends = None
if with_mutual_friends and not data['user'].get('bot', False):
mutual_friends = await state.http.get_mutual_friends(member_id)
return MemberProfile(state=state, data=data, mutual_friends=mutual_friends, guild=self) return MemberProfile(state=state, data=data, guild=self)
async def fetch_ban(self, user: Snowflake) -> BanEntry: async def fetch_ban(self, user: Snowflake) -> BanEntry:
"""|coro| """|coro|

2
discord/http.py

@ -4439,10 +4439,12 @@ class HTTPClient:
guild_id: Optional[Snowflake] = None, guild_id: Optional[Snowflake] = None,
*, *,
with_mutual_guilds: bool = True, with_mutual_guilds: bool = True,
with_mutual_friends: bool = False,
with_mutual_friends_count: bool = False, with_mutual_friends_count: bool = False,
) -> Response[profile.Profile]: ) -> Response[profile.Profile]:
params: Dict[str, Any] = { params: Dict[str, Any] = {
'with_mutual_guilds': str(with_mutual_guilds).lower(), 'with_mutual_guilds': str(with_mutual_guilds).lower(),
'with_mutual_friends': str(with_mutual_friends).lower(),
'with_mutual_friends_count': str(with_mutual_friends_count).lower(), 'with_mutual_friends_count': str(with_mutual_friends_count).lower(),
} }
if guild_id: if guild_id:

4
discord/member.py

@ -291,6 +291,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
avatar: Optional[Asset] avatar: Optional[Asset]
avatar_decoration: Optional[Asset] avatar_decoration: Optional[Asset]
avatar_decoration_sku_id: Optional[int] avatar_decoration_sku_id: Optional[int]
avatar_decoration_expires_at: Optional[datetime.datetime]
note: Note note: Note
relationship: Optional[Relationship] relationship: Optional[Relationship]
is_friend: Callable[[], bool] is_friend: Callable[[], bool]
@ -1131,8 +1132,7 @@ class Member(discord.abc.Messageable, discord.abc.Connectable, _UserTag):
.. versionadded:: 2.0 .. versionadded:: 2.0
with_mutual_friends: :class:`bool` with_mutual_friends: :class:`bool`
Whether to fetch mutual friends. Whether to fetch mutual friends.
This fills in :attr:`MemberProfile.mutual_friends` and :attr:`MemberProfile.mutual_friends_count`, This fills in :attr:`MemberProfile.mutual_friends` and :attr:`MemberProfile.mutual_friends_count`.
but requires an extra API call.
.. versionadded:: 2.0 .. versionadded:: 2.0

42
discord/profile.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, Collection, List, Optional, Tuple
from . import utils from . import utils
from .application import ApplicationInstallParams from .application import ApplicationInstallParams
@ -72,7 +72,7 @@ class Profile:
data: ProfilePayload = kwargs.pop('data') data: ProfilePayload = kwargs.pop('data')
user = data['user'] user = data['user']
profile = data.get('user_profile') profile = data.get('user_profile')
mutual_friends: List[PartialUserPayload] = kwargs.pop('mutual_friends', None) mutual_friends = data.get('mutual_friends')
member = data.get('guild_member') member = data.get('guild_member')
member_profile = data.get('guild_member_profile') member_profile = data.get('guild_member_profile')
@ -89,7 +89,9 @@ class Profile:
super().__init__(**kwargs) super().__init__(**kwargs)
state = self._state state = self._state
# All metadata will be missing on a blocked profile
self.metadata = ProfileMetadata(id=self.id, state=state, data=profile) self.metadata = ProfileMetadata(id=self.id, state=state, data=profile)
self._blocked = profile is None
if member is not None: if member is not None:
self.guild_metadata = ProfileMetadata(id=self.id, state=state, data=member_profile) self.guild_metadata = ProfileMetadata(id=self.id, state=state, data=member_profile)
@ -118,7 +120,7 @@ class Profile:
application = data.get('application') application = data.get('application')
self.application: Optional[ApplicationProfile] = ApplicationProfile(data=application) if application else None self.application: Optional[ApplicationProfile] = ApplicationProfile(data=application) if application else None
def _parse_mutual_friends(self, mutual_friends: List[PartialUserPayload]) -> Optional[List[User]]: def _parse_mutual_friends(self, mutual_friends: Optional[Collection[PartialUserPayload]]) -> Optional[List[User]]:
if self.bot: if self.bot:
# Bots don't have friends # Bots don't have friends
return [] return []
@ -144,6 +146,13 @@ class Profile:
""":class:`bool`: Indicates if the user is a premium user.""" """:class:`bool`: Indicates if the user is a premium user."""
return self.premium_since is not None return self.premium_since is not None
def is_blocked_by_user(self) -> bool:
""":class:`bool`: Indicates if the user has blocked the client user.
.. versionadded:: 2.1
"""
return self._blocked
class ProfileMetadata: class ProfileMetadata:
"""Represents global or per-user Discord profile metadata. """Represents global or per-user Discord profile metadata.
@ -156,8 +165,6 @@ class ProfileMetadata:
The profile's "about me" field. Could be ``None``. The profile's "about me" field. Could be ``None``.
pronouns: Optional[:class:`str`] pronouns: Optional[:class:`str`]
The profile's pronouns, if any. The profile's pronouns, if any.
effect_id: Optional[:class:`int`]
The ID of the profile effect the user has, if any.
""" """
__slots__ = ( __slots__ = (
@ -167,11 +174,12 @@ class ProfileMetadata:
'pronouns', 'pronouns',
'emoji', 'emoji',
'popout_animation_particle_type', 'popout_animation_particle_type',
'effect_id',
'_banner', '_banner',
'_accent_colour', '_accent_colour',
'_theme_colours', '_theme_colours',
'_guild_id', '_guild_id',
'_effect_id',
'_effect_expires_at',
) )
def __init__(self, *, id: int, state: ConnectionState, data: Optional[ProfileMetadataPayload]) -> None: def __init__(self, *, id: int, state: ConnectionState, data: Optional[ProfileMetadataPayload]) -> None:
@ -186,12 +194,15 @@ class ProfileMetadata:
self.pronouns: Optional[str] = data.get('pronouns') or None self.pronouns: Optional[str] = data.get('pronouns') or None
self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict_stateful(data['emoji'], state) if data.get('emoji') else None # type: ignore self.emoji: Optional[PartialEmoji] = PartialEmoji.from_dict_stateful(data['emoji'], state) if data.get('emoji') else None # type: ignore
self.popout_animation_particle_type: Optional[int] = utils._get_as_snowflake(data, 'popout_animation_particle_type') self.popout_animation_particle_type: Optional[int] = utils._get_as_snowflake(data, 'popout_animation_particle_type')
self.effect_id: Optional[int] = utils._get_as_snowflake(data['profile_effect'], 'id') if data.get('profile_effect') else None # type: ignore
self._banner: Optional[str] = data.get('banner') self._banner: Optional[str] = data.get('banner')
self._accent_colour: Optional[int] = data.get('accent_color') self._accent_colour: Optional[int] = data.get('accent_color')
self._theme_colours: Optional[Tuple[int, int]] = tuple(data['theme_colors']) if data.get('theme_colors') else None # type: ignore self._theme_colours: Optional[Tuple[int, int]] = tuple(data['theme_colors']) if data.get('theme_colors') else None # type: ignore
self._guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id') self._guild_id: Optional[int] = utils._get_as_snowflake(data, 'guild_id')
effect_data = data.get('profile_effect')
self._effect_id: Optional[int] = utils._get_as_snowflake(effect_data, 'id') if effect_data else None
self._effect_expires_at = effect_data.get('expires_at') if effect_data else None
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<ProfileMetadata bio={self.bio!r} pronouns={self.pronouns!r}>' return f'<ProfileMetadata bio={self.bio!r} pronouns={self.pronouns!r}>'
@ -248,6 +259,23 @@ class ProfileMetadata:
""" """
return self.theme_colours return self.theme_colours
@property
def effect_id(self) -> Optional[int]:
"""Optional[:class:`int`]: Returns the ID of the profile effect the user has, if any.."""
return self._effect_id
@property
def effect_expires_at(self) -> Optional[datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the profile effect's expiration time.
If the user does not have an expiring profile effect, ``None`` is returned.
.. versionadded:: 2.1
"""
if self._effect_expires_at is None:
return None
return utils.parse_timestamp(self._effect_expires_at, ms=False)
class ApplicationProfile(Hashable): class ApplicationProfile(Hashable):
"""Represents a Discord application profile. """Represents a Discord application profile.

2
discord/types/profile.py

@ -38,6 +38,7 @@ class ProfileUser(APIUser):
class ProfileEffect(TypedDict): class ProfileEffect(TypedDict):
id: Snowflake id: Snowflake
expires_at: Optional[int]
class ProfileMetadata(TypedDict, total=False): class ProfileMetadata(TypedDict, total=False):
@ -82,6 +83,7 @@ class Profile(TypedDict):
guild_member_profile: NotRequired[Optional[ProfileMetadata]] guild_member_profile: NotRequired[Optional[ProfileMetadata]]
guild_badges: List[ProfileBadge] guild_badges: List[ProfileBadge]
mutual_guilds: NotRequired[List[MutualGuild]] mutual_guilds: NotRequired[List[MutualGuild]]
mutual_friends: NotRequired[List[APIUser]]
mutual_friends_count: NotRequired[int] mutual_friends_count: NotRequired[int]
connected_accounts: List[PartialConnection] connected_accounts: List[PartialConnection]
application_role_connections: NotRequired[List[RoleConnection]] application_role_connections: NotRequired[List[RoleConnection]]

1
discord/types/user.py

@ -96,6 +96,7 @@ class User(APIUser, total=False):
class UserAvatarDecorationData(TypedDict): class UserAvatarDecorationData(TypedDict):
asset: str asset: str
sku_id: NotRequired[Snowflake] sku_id: NotRequired[Snowflake]
expires_at: Optional[int]
class PomeloAttempt(TypedDict): class PomeloAttempt(TypedDict):

31
discord/user.py

@ -40,7 +40,15 @@ from .enums import (
from .errors import ClientException, NotFound from .errors import ClientException, NotFound
from .flags import PublicUserFlags, PrivateUserFlags, PremiumUsageFlags, PurchasedFlags from .flags import PublicUserFlags, PrivateUserFlags, PremiumUsageFlags, PurchasedFlags
from .relationship import Relationship from .relationship import Relationship
from .utils import _bytes_to_base64_data, _get_as_snowflake, cached_slot_property, copy_doc, snowflake_time, MISSING from .utils import (
_bytes_to_base64_data,
_get_as_snowflake,
cached_slot_property,
copy_doc,
parse_timestamp,
snowflake_time,
MISSING,
)
from .voice_client import VoiceClient from .voice_client import VoiceClient
if TYPE_CHECKING: if TYPE_CHECKING:
@ -245,6 +253,7 @@ class BaseUser(_UserTag):
'_avatar', '_avatar',
'_avatar_decoration', '_avatar_decoration',
'_avatar_decoration_sku_id', '_avatar_decoration_sku_id',
'_avatar_decoration_expires_at',
'_banner', '_banner',
'_accent_colour', '_accent_colour',
'bot', 'bot',
@ -267,6 +276,7 @@ class BaseUser(_UserTag):
_avatar: Optional[str] _avatar: Optional[str]
_avatar_decoration: Optional[str] _avatar_decoration: Optional[str]
_avatar_decoration_sku_id: Optional[int] _avatar_decoration_sku_id: Optional[int]
_avatar_decoration_expires_at: Optional[int]
_banner: Optional[str] _banner: Optional[str]
_accent_colour: Optional[int] _accent_colour: Optional[int]
_public_flags: int _public_flags: int
@ -311,6 +321,7 @@ class BaseUser(_UserTag):
decoration_data = data.get('avatar_decoration_data') decoration_data = data.get('avatar_decoration_data')
self._avatar_decoration = decoration_data.get('asset') if decoration_data else None self._avatar_decoration = decoration_data.get('asset') if decoration_data else None
self._avatar_decoration_sku_id = _get_as_snowflake(decoration_data, 'sku_id') if decoration_data else None self._avatar_decoration_sku_id = _get_as_snowflake(decoration_data, 'sku_id') if decoration_data else None
self._avatar_decoration_expires_at = decoration_data.get('expires_at') if decoration_data else None
@classmethod @classmethod
def _copy(cls, user: Self) -> Self: def _copy(cls, user: Self) -> Self:
@ -323,6 +334,7 @@ class BaseUser(_UserTag):
self._avatar = user._avatar self._avatar = user._avatar
self._avatar_decoration = user._avatar_decoration self._avatar_decoration = user._avatar_decoration
self._avatar_decoration_sku_id = user._avatar_decoration_sku_id self._avatar_decoration_sku_id = user._avatar_decoration_sku_id
self._avatar_decoration_expires_at = user._avatar_decoration_expires_at
self._banner = user._banner self._banner = user._banner
self._accent_colour = user._accent_colour self._accent_colour = user._accent_colour
self._public_flags = user._public_flags self._public_flags = user._public_flags
@ -335,7 +347,7 @@ class BaseUser(_UserTag):
def _to_minimal_user_json(self) -> APIUserPayload: def _to_minimal_user_json(self) -> APIUserPayload:
decoration: Optional[UserAvatarDecorationData] = None decoration: Optional[UserAvatarDecorationData] = None
if self._avatar_decoration is not None: if self._avatar_decoration is not None:
decoration = {'asset': self._avatar_decoration} decoration = {'asset': self._avatar_decoration, 'expires_at': self._avatar_decoration_expires_at}
if self._avatar_decoration_sku_id is not None: if self._avatar_decoration_sku_id is not None:
decoration['sku_id'] = self._avatar_decoration_sku_id decoration['sku_id'] = self._avatar_decoration_sku_id
@ -425,6 +437,18 @@ class BaseUser(_UserTag):
""" """
return self._avatar_decoration_sku_id return self._avatar_decoration_sku_id
@property
def avatar_decoration_expires_at(self) -> Optional[datetime]:
"""Optional[:class:`datetime.datetime`]: Returns the avatar decoration's expiration time.
If the user does not have an expiring avatar decoration, ``None`` is returned.
.. versionadded:: 2.1
"""
if self._avatar_decoration_expires_at is None:
return None
return parse_timestamp(self._avatar_decoration_expires_at, ms=False)
@property @property
def banner(self) -> Optional[Asset]: def banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the user's banner asset, if available. """Optional[:class:`Asset`]: Returns the user's banner asset, if available.
@ -608,8 +632,7 @@ class BaseUser(_UserTag):
.. versionadded:: 2.0 .. versionadded:: 2.0
with_mutual_friends: :class:`bool` with_mutual_friends: :class:`bool`
Whether to fetch mutual friends. Whether to fetch mutual friends.
This fills in :attr:`UserProfile.mutual_friends` and :attr:`UserProfile.mutual_friends_count`, This fills in :attr:`UserProfile.mutual_friends` and :attr:`UserProfile.mutual_friends_count`.
but requires an extra API call.
.. versionadded:: 2.0 .. versionadded:: 2.0

12
discord/utils.py

@ -330,23 +330,25 @@ def parse_date(date: Optional[str]) -> Optional[datetime.date]:
@overload @overload
def parse_timestamp(timestamp: None) -> None: def parse_timestamp(timestamp: None, *, ms: bool = True) -> None:
... ...
@overload @overload
def parse_timestamp(timestamp: float) -> datetime.datetime: def parse_timestamp(timestamp: float, *, ms: bool = True) -> datetime.datetime:
... ...
@overload @overload
def parse_timestamp(timestamp: Optional[float]) -> Optional[datetime.datetime]: def parse_timestamp(timestamp: Optional[float], *, ms: bool = True) -> Optional[datetime.datetime]:
... ...
def parse_timestamp(timestamp: Optional[float]) -> Optional[datetime.datetime]: def parse_timestamp(timestamp: Optional[float], *, ms: bool = True) -> Optional[datetime.datetime]:
if timestamp: if timestamp:
return datetime.datetime.fromtimestamp(timestamp / 1000.0, tz=datetime.timezone.utc) if ms:
timestamp /= 1000
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
def copy_doc(original: Callable[..., Any]) -> Callable[[T], T]: def copy_doc(original: Callable[..., Any]) -> Callable[[T], T]:

Loading…
Cancel
Save