Skip to content

Commit

Permalink
Expose profile metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
dolfies committed Jan 17, 2024
1 parent 27a8797 commit 29d224e
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 12 deletions.
8 changes: 8 additions & 0 deletions discord/partial_emoji.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ def from_dict(cls, data: Union[PartialEmojiPayload, ActivityEmoji, Dict[str, Any
name=data.get('name') or '',
)

@classmethod
def from_dict_stateful(
cls, data: Union[PartialEmojiPayload, ActivityEmoji, Dict[str, Any]], state: ConnectionState
) -> Self:
self = cls.from_dict(data)
self._state = state
return self

@classmethod
def from_str(cls, value: str) -> Self:
"""Converts a Discord string representation of an emoji to a :class:`PartialEmoji`.
Expand Down
136 changes: 132 additions & 4 deletions discord/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,18 @@

from __future__ import annotations

from typing import TYPE_CHECKING, List, Optional
from typing import TYPE_CHECKING, List, Optional, Tuple

from . import utils
from .application import ApplicationInstallParams
from .asset import Asset, AssetMixin
from .colour import Colour
from .connections import PartialConnection
from .enums import PremiumType, try_enum
from .flags import ApplicationFlags
from .member import Member
from .mixins import Hashable
from .partial_emoji import PartialEmoji
from .user import User

if TYPE_CHECKING:
Expand All @@ -45,11 +47,13 @@
Profile as ProfilePayload,
ProfileApplication as ProfileApplicationPayload,
ProfileBadge as ProfileBadgePayload,
ProfileMetadata as ProfileMetadataPayload,
MutualGuild as MutualGuildPayload,
)
from .types.user import PartialUser as PartialUserPayload

__all__ = (
'ProfileMetadata',
'ApplicationProfile',
'MutualGuild',
'ProfileBadge',
Expand All @@ -71,6 +75,7 @@ def __init__(self, **kwargs) -> None:
mutual_friends: List[PartialUserPayload] = kwargs.pop('mutual_friends', None)

member = data.get('guild_member')
member_profile = data.get('guild_member_profile')
if member is not None:
member['user'] = user
kwargs['data'] = member
Expand All @@ -84,16 +89,19 @@ def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
state = self._state

self.metadata = ProfileMetadata(id=self.id, state=state, data=profile)
if member is not None:
self.guild_metadata = ProfileMetadata(id=self.id, state=state, data=member_profile)

self.legacy_username: Optional[str] = data.get('legacy_username')
self.bio: Optional[str] = user['bio'] or None

# We need to do a bit of a hack here because premium_since is massively overloaded
guild_premium_since = getattr(self, 'premium_since', utils.MISSING)
if guild_premium_since is not utils.MISSING:
self.guild_premium_since = guild_premium_since

self.premium_type: Optional[PremiumType] = (
try_enum(PremiumType, data.get('premium_type') or 0) if profile else None
)
self.premium_type: Optional[PremiumType] = try_enum(PremiumType, data.get('premium_type') or 0) if profile else None
self.premium_since: Optional[datetime] = utils.parse_time(data.get('premium_since'))
self.premium_guild_since: Optional[datetime] = utils.parse_time(data.get('premium_guild_since'))
self.connections: List[PartialConnection] = [PartialConnection(d) for d in data['connected_accounts']]
Expand Down Expand Up @@ -137,6 +145,110 @@ def premium(self) -> bool:
return self.premium_since is not None


class ProfileMetadata:
"""Represents global or per-user Discord profile metadata.
.. versionadded:: 2.1
Attributes
------------
bio: Optional[:class:`str`]
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__ = (
'_id',
'_state',
'bio',
'pronouns',
'emoji',
'popout_animation_particle_type',
'effect_id',
'_banner',
'_accent_colour',
'_theme_colours',
'_guild_id',
)

def __init__(self, *, id: int, state: ConnectionState, data: Optional[ProfileMetadataPayload]) -> None:
self._id = id
self._state = state

# user_profile is null if blocked
if data is None:
data = {'pronouns': ''}

self.bio: Optional[str] = data.get('bio') 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.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')

def __repr__(self) -> str:
return f'<ProfileMetadata bio={self.bio!r} pronouns={self.pronouns!r}>'

@property
def banner(self) -> Optional[Asset]:
"""Optional[:class:`Asset`]: Returns the user's banner asset, if available."""
if self._banner is None:
return None
return Asset._from_user_banner(self._state, self._id, self._banner)

@property
def accent_colour(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: Returns the profile's accent colour, if applicable.
A user's accent colour is only shown if they do not have a banner.
This will only be available if the user explicitly sets a colour.
There is an alias for this named :attr:`accent_color`.
"""
if self._accent_colour is None:
return None
return Colour(self._accent_colour)

@property
def accent_color(self) -> Optional[Colour]:
"""Optional[:class:`Colour`]: Returns the profile's accent color, if applicable.
A user's accent color is only shown if they do not have a banner.
This will only be available if the user explicitly sets a color.
There is an alias for this named :attr:`accent_colour`.
"""
return self.accent_colour

@property
def theme_colours(self) -> Optional[Tuple[Colour, Colour]]:
"""Optional[Tuple[:class:`Colour`, :class:`Colour`]]: Returns the profile's theme colours, if applicable.
The first colour is the user's background colour and the second is the user's foreground colour.
There is an alias for this named :attr:`theme_colors`.
"""
if self._theme_colours is None:
return None
return tuple(Colour(c) for c in self._theme_colours) # type: ignore

Check warning on line 239 in discord/profile.py

View workflow job for this annotation

GitHub Actions / check

Unnecessary "# type: ignore" comment

@property
def theme_colors(self) -> Optional[Tuple[Colour, Colour]]:
"""Optional[Tuple[:class:`Colour`, :class:`Colour`]]: Returns the profile's theme colors, if applicable.
The first color is the user's background color and the second is the user's foreground color.
There is an alias for this named :attr:`theme_colours`.
"""
return self.theme_colours


class ApplicationProfile(Hashable):
"""Represents a Discord application profile.
Expand Down Expand Up @@ -362,6 +474,14 @@ class UserProfile(Profile, User):
-----------
application: Optional[:class:`ApplicationProfile`]
The application profile of the user, if it is a bot.
metadata: :class:`ProfileMetadata`
The global profile metadata of the user.
.. versionadded:: 2.1
legacy_username: Optional[:class:`str`]
The user's legacy username (Username#Discriminator), if public.
.. versionadded:: 2.1
bio: Optional[:class:`str`]
The user's "about me" field. Could be ``None``.
premium_type: Optional[:class:`PremiumType`]
Expand Down Expand Up @@ -449,6 +569,14 @@ class MemberProfile(Profile, Member):
-----------
application: Optional[:class:`ApplicationProfile`]
The application profile of the user, if it is a bot.
metadata: :class:`ProfileMetadata`
The global profile metadata of the user.
.. versionadded:: 2.1
legacy_username: Optional[:class:`str`]
The user's legacy username (Username#Discriminator), if public.
.. versionadded:: 2.1
bio: Optional[:class:`str`]
The user's "about me" field. Could be ``None``.
guild_bio: Optional[:class:`str`]
Expand Down
26 changes: 18 additions & 8 deletions discord/types/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@
DEALINGS IN THE SOFTWARE.
"""

from typing import List, Optional, TypedDict
from typing_extensions import NotRequired
from typing import List, Optional, Tuple, TypedDict
from typing_extensions import NotRequired, Required

from .application import ApplicationInstallParams, RoleConnection
from .emoji import Emoji
from .member import PrivateMember as ProfileMember
from .snowflake import Snowflake
from .user import APIUser, PartialConnection, PremiumType
Expand All @@ -35,12 +36,20 @@ class ProfileUser(APIUser):
bio: str


class ProfileMetadata(TypedDict):
guild_id: NotRequired[int]
bio: NotRequired[str]
banner: NotRequired[Optional[str]]
accent_color: NotRequired[Optional[int]]
theme_colors: NotRequired[List[int]]
class ProfileEffect(TypedDict):
id: Snowflake


class ProfileMetadata(TypedDict, total=False):
guild_id: int
bio: str
banner: Optional[str]
accent_color: Optional[int]
theme_colors: Optional[Tuple[int, int]]
emoji: Optional[Emoji]
popout_animation_particle_type: Optional[Snowflake]
profile_effect: Optional[ProfileEffect]
pronouns: Required[str]


class MutualGuild(TypedDict):
Expand Down Expand Up @@ -79,4 +88,5 @@ class Profile(TypedDict):
premium_type: Optional[PremiumType]
premium_since: Optional[str]
premium_guild_since: Optional[str]
legacy_username: Optional[str]
application: NotRequired[ProfileApplication]
5 changes: 5 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6826,6 +6826,11 @@ User
:members:
:inherited-members:

.. attributetable:: ProfileMetadata

.. autoclass:: ProfileMetadata()
:members:

.. attributetable:: ProfileBadge

.. autoclass:: ProfileBadge()
Expand Down

0 comments on commit 29d224e

Please sign in to comment.