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
@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
def default_avatar(self) -> Asset:
""":class:`~discord.Asset`: Returns the default avatar for a given user."""

15
discord/client.py

@ -2523,8 +2523,7 @@ class Client:
.. versionadded:: 2.0
with_mutual_friends: :class:`bool`
Whether to fetch mutual friends.
This fills in :attr:`.UserProfile.mutual_friends` and :attr:`.UserProfile.mutual_friends_count`,
but requires an extra API call.
This fills in :attr:`.UserProfile.mutual_friends` and :attr:`.UserProfile.mutual_friends_count`.
.. versionadded:: 2.0
@ -2533,7 +2532,7 @@ class Client:
NotFound
A user with this ID does not exist.
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
Fetching the profile failed.
@ -2544,13 +2543,13 @@ class Client:
"""
state = self._connection
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]:
"""|coro|

9
discord/guild.py

@ -2702,8 +2702,7 @@ class Guild(Hashable):
This fills in :attr:`.MemberProfile.mutual_friends_count`.
with_mutual_friends: :class:`bool`
Whether to fetch mutual friends.
This fills in :attr:`.MemberProfile.mutual_friends` and :attr:`.MemberProfile.mutual_friends_count`,
but requires an extra API call.
This fills in :attr:`.MemberProfile.mutual_friends` and :attr:`.MemberProfile.mutual_friends_count`.
Raises
-------
@ -2727,16 +2726,14 @@ class Guild(Hashable):
self.id,
with_mutual_guilds=with_mutual_guilds,
with_mutual_friends_count=with_mutual_friends_count,
with_mutual_friends=with_mutual_friends,
)
if 'guild_member_profile' not in data:
raise InvalidData('Member is not in this guild')
if 'guild_member' not in data:
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:
"""|coro|

2
discord/http.py

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

4
discord/member.py

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

42
discord/profile.py

@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
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 .application import ApplicationInstallParams
@ -72,7 +72,7 @@ class Profile:
data: ProfilePayload = kwargs.pop('data')
user = data['user']
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_profile = data.get('guild_member_profile')
@ -89,7 +89,9 @@ class Profile:
super().__init__(**kwargs)
state = self._state
# All metadata will be missing on a blocked profile
self.metadata = ProfileMetadata(id=self.id, state=state, data=profile)
self._blocked = profile is None
if member is not None:
self.guild_metadata = ProfileMetadata(id=self.id, state=state, data=member_profile)
@ -118,7 +120,7 @@ class Profile:
application = data.get('application')
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:
# Bots don't have friends
return []
@ -144,6 +146,13 @@ class Profile:
""":class:`bool`: Indicates if the user is a premium user."""
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:
"""Represents global or per-user Discord profile metadata.
@ -156,8 +165,6 @@ class ProfileMetadata:
The profile's "about me" field. Could be ``None``.
pronouns: Optional[:class:`str`]
The profile's pronouns, if any.
effect_id: Optional[:class:`int`]
The ID of the profile effect the user has, if any.
"""
__slots__ = (
@ -167,11 +174,12 @@ class ProfileMetadata:
'pronouns',
'emoji',
'popout_animation_particle_type',
'effect_id',
'_banner',
'_accent_colour',
'_theme_colours',
'_guild_id',
'_effect_id',
'_effect_expires_at',
)
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.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.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._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._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:
return f'<ProfileMetadata bio={self.bio!r} pronouns={self.pronouns!r}>'
@ -248,6 +259,23 @@ class ProfileMetadata:
"""
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):
"""Represents a Discord application profile.

2
discord/types/profile.py

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

1
discord/types/user.py

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

31
discord/user.py

@ -40,7 +40,15 @@ from .enums import (
from .errors import ClientException, NotFound
from .flags import PublicUserFlags, PrivateUserFlags, PremiumUsageFlags, PurchasedFlags
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
if TYPE_CHECKING:
@ -245,6 +253,7 @@ class BaseUser(_UserTag):
'_avatar',
'_avatar_decoration',
'_avatar_decoration_sku_id',
'_avatar_decoration_expires_at',
'_banner',
'_accent_colour',
'bot',
@ -267,6 +276,7 @@ class BaseUser(_UserTag):
_avatar: Optional[str]
_avatar_decoration: Optional[str]
_avatar_decoration_sku_id: Optional[int]
_avatar_decoration_expires_at: Optional[int]
_banner: Optional[str]
_accent_colour: Optional[int]
_public_flags: int
@ -311,6 +321,7 @@ class BaseUser(_UserTag):
decoration_data = data.get('avatar_decoration_data')
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_expires_at = decoration_data.get('expires_at') if decoration_data else None
@classmethod
def _copy(cls, user: Self) -> Self:
@ -323,6 +334,7 @@ class BaseUser(_UserTag):
self._avatar = user._avatar
self._avatar_decoration = user._avatar_decoration
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._accent_colour = user._accent_colour
self._public_flags = user._public_flags
@ -335,7 +347,7 @@ class BaseUser(_UserTag):
def _to_minimal_user_json(self) -> APIUserPayload:
decoration: Optional[UserAvatarDecorationData] = 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:
decoration['sku_id'] = self._avatar_decoration_sku_id
@ -425,6 +437,18 @@ class BaseUser(_UserTag):
"""
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
def banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the user's banner asset, if available.
@ -608,8 +632,7 @@ class BaseUser(_UserTag):
.. versionadded:: 2.0
with_mutual_friends: :class:`bool`
Whether to fetch mutual friends.
This fills in :attr:`UserProfile.mutual_friends` and :attr:`UserProfile.mutual_friends_count`,
but requires an extra API call.
This fills in :attr:`UserProfile.mutual_friends` and :attr:`UserProfile.mutual_friends_count`.
.. versionadded:: 2.0

12
discord/utils.py

@ -330,23 +330,25 @@ def parse_date(date: Optional[str]) -> Optional[datetime.date]:
@overload
def parse_timestamp(timestamp: None) -> None:
def parse_timestamp(timestamp: None, *, ms: bool = True) -> None:
...
@overload
def parse_timestamp(timestamp: float) -> datetime.datetime:
def parse_timestamp(timestamp: float, *, ms: bool = True) -> datetime.datetime:
...
@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:
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]:

Loading…
Cancel
Save