""" The MIT License (MIT) Copyright (c) 2021-present Dolfies Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from __future__ import annotations 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: from datetime import datetime from .guild import Guild from .state import ConnectionState from .types.profile import ( 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', 'UserProfile', 'MemberProfile', ) class Profile: if TYPE_CHECKING: id: int bot: bool _state: ConnectionState def __init__(self, **kwargs) -> None: data: ProfilePayload = kwargs.pop('data') user = data['user'] profile = data.get('user_profile') 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 else: kwargs['data'] = user # n.b. this class is subclassed by UserProfile and MemberProfile # which subclass either User or Member respectively # Because of this, we call super().__init__ here # after ensuring the data kwarg is set correctly 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_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']] self.badges: List[ProfileBadge] = [ ProfileBadge(state=state, data=d) for d in data.get('badges', []) + data.get('guild_badges', []) ] self.mutual_guilds: Optional[List[MutualGuild]] = ( [MutualGuild(state=state, data=d) for d in data['mutual_guilds']] if 'mutual_guilds' in data else None ) self.mutual_friends: Optional[List[User]] = self._parse_mutual_friends(mutual_friends) self._mutual_friends_count: Optional[int] = data.get('mutual_friends_count') 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]]: if self.bot: # Bots don't have friends return [] if mutual_friends is None: return state = self._state return [state.store_user(friend) for friend in mutual_friends] @property def mutual_friends_count(self) -> Optional[int]: """Optional[:class:`int`]: The number of mutual friends the user has with the client user.""" if self.bot: # Bots don't have friends return 0 if self._mutual_friends_count is not None: return self._mutual_friends_count if self.mutual_friends is not None: return len(self.mutual_friends) @property def premium(self) -> bool: """:class:`bool`: Indicates if the user is a premium user.""" 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'' @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 @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. .. container:: operations .. describe:: x == y Checks if two applications are equal. .. describe:: x != y Checks if two applications are not equal. .. describe:: hash(x) Return the applications's hash. .. versionadded:: 2.0 Attributes ------------ id: :class:`int` The application's ID. verified: :class:`bool` Indicates if the application is verified. popular_application_command_ids: List[:class:`int`] A list of the IDs of the application's popular commands. primary_sku_id: Optional[:class:`int`] The application's primary SKU ID, if any. This can be an application's game SKU, subscription SKU, etc. custom_install_url: Optional[:class:`str`] The custom URL to use for authorizing the application, if specified. install_params: Optional[:class:`ApplicationInstallParams`] The parameters to use for authorizing the application, if specified. """ __slots__ = ( 'id', 'verified', 'popular_application_command_ids', 'primary_sku_id', '_flags', 'custom_install_url', 'install_params', ) def __init__(self, data: ProfileApplicationPayload) -> None: self.id: int = int(data['id']) self.verified: bool = data.get('verified', False) self.popular_application_command_ids: List[int] = [int(id) for id in data.get('popular_application_command_ids', [])] self.primary_sku_id: Optional[int] = utils._get_as_snowflake(data, 'primary_sku_id') self._flags: int = data.get('flags', 0) params = data.get('install_params') self.custom_install_url: Optional[str] = data.get('custom_install_url') self.install_params: Optional[ApplicationInstallParams] = ( ApplicationInstallParams.from_application(self, params) if params else None ) def __repr__(self) -> str: return f'' @property def created_at(self) -> datetime: """:class:`datetime.datetime`: Returns the application's creation time in UTC. .. versionadded:: 2.1 """ return utils.snowflake_time(self.id) @property def flags(self) -> ApplicationFlags: """:class:`ApplicationFlags`: The flags of this application.""" return ApplicationFlags._from_value(self._flags) @property def install_url(self) -> Optional[str]: """:class:`str`: The URL to install the application.""" return self.custom_install_url or self.install_params.url if self.install_params else None @property def primary_sku_url(self) -> Optional[str]: """:class:`str`: The URL to the primary SKU of the application, if any.""" if self.primary_sku_id: return f'https://discord.com/store/skus/{self.primary_sku_id}/unknown' class MutualGuild(Hashable): """Represents a mutual guild between a user and the client user. .. container:: operations .. describe:: x == y Checks if two guilds are equal. .. describe:: x != y Checks if two guilds are not equal. .. describe:: hash(x) Returns the guild's hash. .. versionadded:: 2.0 Attributes ------------ id: :class:`int` The guild's ID. nick: Optional[:class:`str`] The guild specific nickname of the user. """ __slots__ = ('id', 'nick', '_state') def __init__(self, *, state: ConnectionState, data: MutualGuildPayload) -> None: self._state = state self.id: int = int(data['id']) self.nick: Optional[str] = data.get('nick') def __repr__(self) -> str: return f'' @property def guild(self) -> Guild: """:class:`Guild`: The guild that the user is mutual with.""" return self._state._get_or_create_unavailable_guild(self.id) class ProfileBadge(AssetMixin, Hashable): """Represents a Discord profile badge. .. container:: operations .. describe:: x == y Checks if two badges are equal. .. describe:: x != y Checks if two badges are not equal. .. describe:: hash(x) Returns the badge's hash. .. describe:: str(x) Returns the badge's description. .. versionadded:: 2.1 Attributes ------------ id: :class:`str` The badge's ID. description: :class:`str` The badge's description. link: Optional[:class:`str`] The link associated with the badge, if any. """ __slots__ = ('id', 'description', 'link', '_icon', '_state') def __init__(self, *, state: ConnectionState, data: ProfileBadgePayload) -> None: self._state = state self.id: str = data['id'] self.description: str = data.get('description', '') self.link: Optional[str] = data.get('link') self._icon: str = data['icon'] def __repr__(self) -> str: return f'' def __hash__(self) -> int: return hash(self.id) def __str__(self) -> str: return self.description @property def animated(self) -> bool: """:class:`bool`: Indicates if the badge is animated. Here for compatibility purposes.""" return False @property def url(self) -> str: """:class:`str`: Returns the URL of the badge icon.""" return f'{Asset.BASE}/badge-icons/{self._icon}.png' class UserProfile(Profile, User): """Represents a Discord user's profile. This is a :class:`User` with extended attributes. .. container:: operations .. describe:: x == y Checks if two users are equal. .. describe:: x != y Checks if two users are not equal. .. describe:: hash(x) Return the user's hash. .. describe:: str(x) Returns the user's name with discriminator. .. note:: Information may be missing or inaccurate if the user has blocked the client user. .. versionadded:: 2.0 Attributes ----------- 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`] Specifies the type of premium a user has (i.e. Nitro, Nitro Classic, or Nitro Basic). .. versionchanged:: 2.1 This is now :attr:`PremiumType.none` instead of ``None`` if the user is not premium. premium_since: Optional[:class:`datetime.datetime`] An aware datetime object that specifies how long a user has been premium (had Nitro). ``None`` if the user is not a premium user. premium_guild_since: Optional[:class:`datetime.datetime`] An aware datetime object that specifies when a user first Nitro boosted a guild. connections: Optional[List[:class:`PartialConnection`]] The connected accounts that show up on the profile. badges: List[:class:`ProfileBadge`] A list of badge icons that the user has. .. versionadded:: 2.1 mutual_guilds: Optional[List[:class:`MutualGuild`]] A list of guilds that you share with the user. ``None`` if you didn't fetch mutual guilds. mutual_friends: Optional[List[:class:`User`]] A list of friends that you share with the user. ``None`` if you didn't fetch mutual friends. """ __slots__ = ( 'bio', 'premium_type', 'premium_since', 'premium_guild_since', 'connections', 'badges', 'mutual_guilds', 'mutual_friends', '_mutual_friends_count', 'application', ) def __repr__(self) -> str: return f'' @property def display_bio(self) -> Optional[str]: """Optional[:class:`str`]: Returns the user's display bio. This is the same as :attr:`bio` and is here for compatibility. """ return self.bio class MemberProfile(Profile, Member): """Represents a Discord member's profile. This is a :class:`Member` with extended attributes. .. container:: operations .. describe:: x == y Checks if two members are equal. Note that this works with :class:`User` instances too. .. describe:: x != y Checks if two members are not equal. Note that this works with :class:`User` instances too. .. describe:: hash(x) Returns the member's hash. .. describe:: str(x) Returns the member's name with the discriminator. .. note:: Information may be missing or inaccurate if the user has blocked the client user. .. versionadded:: 2.0 Attributes ----------- 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`] The user's "about me" field for the guild. Could be ``None``. guild_premium_since: Optional[:class:`datetime.datetime`] An aware datetime object that specifies the date and time in UTC when the member used their "Nitro boost" on the guild, if available. This could be ``None``. .. note:: This is renamed from :attr:`Member.premium_since` because of name collisions. premium_type: Optional[:class:`PremiumType`] Specifies the type of premium a user has (i.e. Nitro, Nitro Classic, or Nitro Basic). .. versionchanged:: 2.1 This is now :attr:`PremiumType.none` instead of ``None`` if the user is not premium. premium_since: Optional[:class:`datetime.datetime`] An aware datetime object that specifies how long a user has been premium (had Nitro). ``None`` if the user is not a premium user. .. note:: This is not the same as :attr:`Member.premium_since`. That is renamed to :attr:`guild_premium_since`. premium_guild_since: Optional[:class:`datetime.datetime`] An aware datetime object that specifies when a user first Nitro boosted a guild. connections: Optional[List[:class:`PartialConnection`]] The connected accounts that show up on the profile. badges: List[:class:`ProfileBadge`] A list of badge icons that the user has. .. versionadded:: 2.1 mutual_guilds: Optional[List[:class:`MutualGuild`]] A list of guilds that you share with the user. ``None`` if you didn't fetch mutuals. mutual_friends: Optional[List[:class:`User`]] A list of friends that you share with the user. ``None`` if you didn't fetch mutuals. """ __slots__ = ( 'bio', 'guild_premium_since', 'premium_type', 'premium_since', 'premium_guild_since', 'connections', 'badges', 'mutual_guilds', 'mutual_friends', '_mutual_friends_count', 'application', '_banner', 'guild_bio', ) def __init__(self, **kwargs): super().__init__(**kwargs) data = kwargs['data'] member = data['guild_member'] self._banner: Optional[str] = member.get('banner') self.guild_bio: Optional[str] = member.get('bio') or None def __repr__(self) -> str: return ( f'' ) @property def display_banner(self) -> Optional[Asset]: """Optional[:class:`Asset`]: Returns the member's display banner. For regular members this is just their banner (if available), but if they have a guild specific banner then that is returned instead. """ return self.guild_banner or self._user.banner @property def guild_banner(self) -> Optional[Asset]: """Optional[:class:`Asset`]: Returns an :class:`Asset` for the guild banner the member has. If unavailable, ``None`` is returned. """ if self._banner is None: return None return Asset._from_guild_banner(self._state, self.guild.id, self.id, self._banner) @property def display_bio(self) -> Optional[str]: """Optional[:class:`str`]: Returns the member's display bio. For regular members this is just their bio (if available), but if they have a guild specific bio then that is returned instead. """ return self.guild_bio or self.bio