# SPDX-License-Identifier: MIT from __future__ import annotations from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union import disnake.abc from .asset import Asset from .colour import Colour from .enums import Locale, try_enum from .flags import PublicUserFlags from .utils import MISSING, _assetbytes_to_base64_data, snowflake_time if TYPE_CHECKING: from datetime import datetime from typing_extensions import Self from .asset import AssetBytes from .channel import DMChannel from .guild import Guild from .message import Message from .state import ConnectionState from .types.channel import DMChannel as DMChannelPayload from .types.user import PartialUser as PartialUserPayload, User as UserPayload __all__ = ( "User", "ClientUser", ) class _UserTag: __slots__ = () id: int class BaseUser(_UserTag): __slots__ = ( "name", "id", "discriminator", "global_name", "_avatar", "_banner", "_accent_colour", "bot", "system", "_public_flags", "_state", ) if TYPE_CHECKING: name: str id: int discriminator: str global_name: Optional[str] bot: bool system: bool _state: ConnectionState _avatar: Optional[str] _banner: Optional[str] _accent_colour: Optional[str] _public_flags: int def __init__( self, *, state: ConnectionState, data: Union[UserPayload, PartialUserPayload] ) -> None: self._state = state self._update(data) def __repr__(self) -> str: return ( f"" ) def __str__(self) -> str: discriminator = self.discriminator if discriminator == "0": return self.name # legacy behavior return f"{self.name}#{discriminator}" def __eq__(self, other: Any) -> bool: return isinstance(other, _UserTag) and other.id == self.id def __ne__(self, other: Any) -> bool: return not self.__eq__(other) def __hash__(self) -> int: return self.id >> 22 def _update(self, data: Union[UserPayload, PartialUserPayload]) -> None: self.name = data["username"] self.id = int(data["id"]) self.discriminator = data["discriminator"] self.global_name = data.get("global_name") self._avatar = data["avatar"] self._banner = data.get("banner", None) self._accent_colour = data.get("accent_color", None) self._public_flags = data.get("public_flags", 0) self.bot = data.get("bot", False) self.system = data.get("system", False) @classmethod def _copy(cls, user: BaseUser) -> Self: self = cls.__new__(cls) # bypass __init__ self.name = user.name self.id = user.id self.discriminator = user.discriminator self.global_name = user.global_name self._avatar = user._avatar self._banner = user._banner self._accent_colour = user._accent_colour self.bot = user.bot self._state = user._state self._public_flags = user._public_flags return self def _to_minimal_user_json(self) -> UserPayload: return { "username": self.name, "id": self.id, "avatar": self._avatar, "discriminator": self.discriminator, "global_name": self.global_name, "bot": self.bot, "public_flags": self._public_flags, } @property def public_flags(self) -> PublicUserFlags: """:class:`PublicUserFlags`: The publicly available flags the user has.""" return PublicUserFlags._from_value(self._public_flags) @property def avatar(self) -> Optional[Asset]: """Optional[:class:`Asset`]: Returns an :class:`Asset` for the avatar the user has. If the user does not have a traditional avatar, ``None`` is returned. If you want the avatar that a user has displayed, consider :attr:`display_avatar`. """ if self._avatar is not None: return Asset._from_avatar(self._state, self.id, self._avatar) return None @property def default_avatar(self) -> Asset: """:class:`Asset`: Returns the default avatar for a given user. .. versionchanged:: 2.9 Added handling for users migrated to the new username system without discriminators. """ if self.discriminator == "0": index = (self.id >> 22) % 6 else: # legacy behavior index = int(self.discriminator) % 5 return Asset._from_default_avatar(self._state, index) @property def display_avatar(self) -> Asset: """:class:`Asset`: Returns the user's display avatar. For regular users this is just their default avatar or uploaded avatar. .. versionadded:: 2.0 """ return self.avatar or self.default_avatar @property def banner(self) -> Optional[Asset]: """Optional[:class:`Asset`]: Returns the user's banner asset, if available. .. versionadded:: 2.0 .. note:: This information is only available via :meth:`Client.fetch_user`. """ if self._banner is None: return None return Asset._from_banner(self._state, self.id, self._banner) @property def accent_colour(self) -> Optional[Colour]: """Optional[:class:`Colour`]: Returns the user's accent colour, if applicable. There is an alias for this named :attr:`accent_color`. .. versionadded:: 2.0 .. note:: This information is only available via :meth:`Client.fetch_user`. """ if self._accent_colour is None: return None return Colour(int(self._accent_colour)) @property def accent_color(self) -> Optional[Colour]: """Optional[:class:`Colour`]: Returns the user's accent color, if applicable. There is an alias for this named :attr:`accent_colour`. .. versionadded:: 2.0 .. note:: This information is only available via :meth:`Client.fetch_user`. """ return self.accent_colour @property def colour(self) -> Colour: """:class:`Colour`: A property that returns a colour denoting the rendered colour for the user. This always returns :meth:`Colour.default`. There is an alias for this named :attr:`color`. """ return Colour.default() @property def color(self) -> Colour: """:class:`Colour`: A property that returns a color denoting the rendered color for the user. This always returns :meth:`Colour.default`. There is an alias for this named :attr:`colour`. """ return self.colour @property def mention(self) -> str: """:class:`str`: Returns a string that allows you to mention the given user.""" return f"<@{self.id}>" @property def created_at(self) -> datetime: """:class:`datetime.datetime`: Returns the user's creation time in UTC. This is when the user's Discord account was created. """ return snowflake_time(self.id) @property def display_name(self) -> str: """:class:`str`: Returns the user's display name. This is their :attr:`global name <.global_name>` if set, or their :attr:`username <.name>` otherwise. .. versionchanged:: 2.9 Added :attr:`.global_name`. """ return self.global_name or self.name def mentioned_in(self, message: Message) -> bool: """Checks if the user is mentioned in the specified message. Parameters ---------- message: :class:`Message` The message to check. Returns ------- :class:`bool` Indicates if the user is mentioned in the message. """ if message.mention_everyone: return True return any(user.id == self.id for user in message.mentions) class ClientUser(BaseUser): """Represents your Discord user. .. 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 username (with discriminator, if not migrated to new system yet). Attributes ---------- name: :class:`str` The user's username. id: :class:`int` The user's unique ID. discriminator: :class:`str` The user's discriminator. global_name: Optional[:class:`str`] The user's global display name, if set. This takes precedence over :attr:`.name` when shown. .. versionadded:: 2.9 bot: :class:`bool` Specifies if the user is a bot account. system: :class:`bool` Specifies if the user is a system user (i.e. represents Discord officially). .. versionadded:: 1.3 verified: :class:`bool` Specifies if the user's email is verified. locale: Optional[:class:`Locale`] The IETF language tag used to identify the language the user is using. .. versionchanged:: 2.5 Changed to :class:`Locale` instead of :class:`str`. mfa_enabled: :class:`bool` Specifies if the user has MFA turned on and working. """ __slots__ = ("locale", "_flags", "verified", "mfa_enabled", "__weakref__") if TYPE_CHECKING: verified: bool locale: Optional[Locale] mfa_enabled: bool _flags: int def __init__(self, *, state: ConnectionState, data: UserPayload) -> None: super().__init__(state=state, data=data) def __repr__(self) -> str: return ( f"" ) def _update(self, data: UserPayload) -> None: super()._update(data) self.verified = data.get("verified", False) locale = data.get("locale") self.locale = try_enum(Locale, locale) if locale else None self._flags = data.get("flags", 0) self.mfa_enabled = data.get("mfa_enabled", False) async def edit( self, *, username: str = MISSING, avatar: Optional[AssetBytes] = MISSING ) -> ClientUser: """|coro| Edits the current profile of the client. .. note:: To upload an avatar, a resource (see below) or a :term:`py:bytes-like object` must be passed in that represents the image being uploaded. The only image formats supported for uploading are JPG and PNG. .. versionchanged:: 2.0 The edit is no longer in-place, instead the newly edited client user is returned. .. versionchanged:: 2.6 Raises :exc:`ValueError` instead of ``InvalidArgument``. Parameters ---------- username: :class:`str` The new username you wish to change to. avatar: Optional[|resource_type|] A :term:`py:bytes-like object` or asset representing the image to upload. Could be ``None`` to denote no avatar. .. versionchanged:: 2.5 Now accepts various resource types in addition to :class:`bytes`. Raises ------ NotFound The ``avatar`` asset couldn't be found. HTTPException Editing your profile failed. TypeError The ``avatar`` asset is a lottie sticker (see :func:`Sticker.read`). ValueError Wrong image format passed for ``avatar``. Returns ------- :class:`ClientUser` The newly edited client user. """ payload: Dict[str, Any] = {} if username is not MISSING: payload["username"] = username if avatar is not MISSING: payload["avatar"] = await _assetbytes_to_base64_data(avatar) data: UserPayload = await self._state.http.edit_profile(payload) return ClientUser(state=self._state, data=data) class User(BaseUser, disnake.abc.Messageable): """Represents a Discord user. .. 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 username (with discriminator, if not migrated to new system yet). Attributes ---------- name: :class:`str` The user's username. id: :class:`int` The user's unique ID. discriminator: :class:`str` The user's discriminator. .. note:: This is being phased out by Discord; the username system is moving away from ``username#discriminator`` to users having a globally unique username. The value of a single zero (``"0"``) indicates that the user has been migrated to the new system. See the `help article `__ for details. global_name: Optional[:class:`str`] The user's global display name, if set. This takes precedence over :attr:`.name` when shown. .. versionadded:: 2.9 bot: :class:`bool` Specifies if the user is a bot account. system: :class:`bool` Specifies if the user is a system user (i.e. represents Discord officially). """ __slots__ = ("__weakref__",) def __repr__(self) -> str: return ( f"" ) async def _get_channel(self) -> DMChannel: ch = await self.create_dm() return ch @property def dm_channel(self) -> Optional[DMChannel]: """Optional[:class:`DMChannel`]: Returns the channel associated with this user if it exists. If this returns ``None``, you can create a DM channel by calling the :meth:`create_dm` coroutine function. """ return self._state._get_private_channel_by_user(self.id) @property def mutual_guilds(self) -> List[Guild]: """List[:class:`Guild`]: The guilds that the user shares with the client. .. note:: This will only return mutual guilds within the client's internal cache. .. versionadded:: 1.7 """ return [guild for guild in self._state._guilds.values() if guild.get_member(self.id)] async def create_dm(self) -> DMChannel: """|coro| Creates a :class:`DMChannel` with this user. This should be rarely called, as this is done transparently for most people. Returns ------- :class:`.DMChannel` The channel that was created. """ found = self.dm_channel if found is not None: return found state = self._state data: DMChannelPayload = await state.http.start_private_message(self.id) return state.add_dm_channel(data)