506 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			506 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
"""
 | 
						|
The MIT License (MIT)
 | 
						|
 | 
						|
Copyright (c) 2015-present Rapptz
 | 
						|
 | 
						|
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 Any, Dict, List, Optional, Type, TypeVar, TYPE_CHECKING
 | 
						|
 | 
						|
import discord.abc
 | 
						|
from .asset import Asset
 | 
						|
from .colour import Colour
 | 
						|
from .enums import DefaultAvatar
 | 
						|
from .flags import PublicUserFlags
 | 
						|
from .utils import snowflake_time, _bytes_to_base64_data, MISSING
 | 
						|
 | 
						|
if TYPE_CHECKING:
 | 
						|
    from datetime import datetime
 | 
						|
 | 
						|
    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 User as UserPayload
 | 
						|
 | 
						|
 | 
						|
__all__ = (
 | 
						|
    'User',
 | 
						|
    'ClientUser',
 | 
						|
)
 | 
						|
 | 
						|
BU = TypeVar('BU', bound='BaseUser')
 | 
						|
 | 
						|
 | 
						|
class _UserTag:
 | 
						|
    __slots__ = ()
 | 
						|
    id: int
 | 
						|
 | 
						|
 | 
						|
class BaseUser(_UserTag):
 | 
						|
    __slots__ = (
 | 
						|
        'name',
 | 
						|
        'id',
 | 
						|
        'discriminator',
 | 
						|
        '_avatar',
 | 
						|
        '_banner',
 | 
						|
        '_accent_colour',
 | 
						|
        'bot',
 | 
						|
        'system',
 | 
						|
        '_public_flags',
 | 
						|
        '_state',
 | 
						|
    )
 | 
						|
 | 
						|
    if TYPE_CHECKING:
 | 
						|
        name: str
 | 
						|
        id: int
 | 
						|
        discriminator: 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: UserPayload) -> None:
 | 
						|
        self._state = state
 | 
						|
        self._update(data)
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return (
 | 
						|
            f"<BaseUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}"
 | 
						|
            f" bot={self.bot} system={self.system}>"
 | 
						|
        )
 | 
						|
 | 
						|
    def __str__(self) -> str:
 | 
						|
        return f'{self.name}#{self.discriminator}'
 | 
						|
 | 
						|
    def __int__(self) -> int:
 | 
						|
        return self.id
 | 
						|
 | 
						|
    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: UserPayload) -> None:
 | 
						|
        self.name = data['username']
 | 
						|
        self.id = int(data['id'])
 | 
						|
        self.discriminator = data['discriminator']
 | 
						|
        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: Type[BU], user: BU) -> BU:
 | 
						|
        self = cls.__new__(cls)  # bypass __init__
 | 
						|
 | 
						|
        self.name = user.name
 | 
						|
        self.id = user.id
 | 
						|
        self.discriminator = user.discriminator
 | 
						|
        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) -> Dict[str, Any]:
 | 
						|
        return {
 | 
						|
            'username': self.name,
 | 
						|
            'id': self.id,
 | 
						|
            'avatar': self._avatar,
 | 
						|
            'discriminator': self.discriminator,
 | 
						|
            'bot': self.bot,
 | 
						|
        }
 | 
						|
 | 
						|
    @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. This is calculated by the user's discriminator."""
 | 
						|
        return Asset._from_default_avatar(self._state, int(self.discriminator) % len(DefaultAvatar))
 | 
						|
 | 
						|
    @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_user_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(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.
 | 
						|
 | 
						|
        For regular users this is just their username, but
 | 
						|
        if they have a guild specific nickname then that
 | 
						|
        is returned instead.
 | 
						|
        """
 | 
						|
        return 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 if you're mentioned in.
 | 
						|
 | 
						|
        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 name with discriminator.
 | 
						|
 | 
						|
    Attributes
 | 
						|
    -----------
 | 
						|
    name: :class:`str`
 | 
						|
        The user's username.
 | 
						|
    id: :class:`int`
 | 
						|
        The user's unique ID.
 | 
						|
    discriminator: :class:`str`
 | 
						|
        The user's discriminator. This is given when the username has conflicts.
 | 
						|
    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:`str`]
 | 
						|
        The IETF language tag used to identify the language the user is using.
 | 
						|
    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[str]
 | 
						|
        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'<ClientUser id={self.id} name={self.name!r} discriminator={self.discriminator!r}'
 | 
						|
            f' bot={self.bot} verified={self.verified} mfa_enabled={self.mfa_enabled}>'
 | 
						|
        )
 | 
						|
 | 
						|
    def _update(self, data: UserPayload) -> None:
 | 
						|
        super()._update(data)
 | 
						|
        # There's actually an Optional[str] phone field as well but I won't use it
 | 
						|
        self.verified = data.get('verified', False)
 | 
						|
        self.locale = data.get('locale')
 | 
						|
        self._flags = data.get('flags', 0)
 | 
						|
        self.mfa_enabled = data.get('mfa_enabled', False)
 | 
						|
 | 
						|
    async def edit(self, *, username: str = MISSING, avatar: bytes = MISSING) -> ClientUser:
 | 
						|
        """|coro|
 | 
						|
 | 
						|
        Edits the current profile of the client.
 | 
						|
 | 
						|
        .. note::
 | 
						|
 | 
						|
            To upload an avatar, a :term:`py:bytes-like object` must be passed in that
 | 
						|
            represents the image being uploaded. If this is done through a file
 | 
						|
            then the file must be opened via ``open('some_filename', 'rb')`` and
 | 
						|
            the :term:`py:bytes-like object` is given through the use of ``fp.read()``.
 | 
						|
 | 
						|
            The only image formats supported for uploading is JPEG and PNG.
 | 
						|
 | 
						|
        .. versionchanged:: 2.0
 | 
						|
            The edit is no longer in-place, instead the newly edited client user is returned.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        -----------
 | 
						|
        username: :class:`str`
 | 
						|
            The new username you wish to change to.
 | 
						|
        avatar: :class:`bytes`
 | 
						|
            A :term:`py:bytes-like object` representing the image to upload.
 | 
						|
            Could be ``None`` to denote no avatar.
 | 
						|
 | 
						|
        Raises
 | 
						|
        ------
 | 
						|
        HTTPException
 | 
						|
            Editing your profile failed.
 | 
						|
        InvalidArgument
 | 
						|
            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'] = _bytes_to_base64_data(avatar)
 | 
						|
 | 
						|
        data: UserPayload = await self._state.http.edit_profile(payload)
 | 
						|
        return ClientUser(state=self._state, data=data)
 | 
						|
 | 
						|
 | 
						|
class User(BaseUser, discord.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 name with discriminator.
 | 
						|
 | 
						|
        .. describe:: int(x)
 | 
						|
 | 
						|
            Returns the user's ID.
 | 
						|
 | 
						|
    Attributes
 | 
						|
    -----------
 | 
						|
    name: :class:`str`
 | 
						|
        The user's username.
 | 
						|
    id: :class:`int`
 | 
						|
        The user's unique ID.
 | 
						|
    discriminator: :class:`str`
 | 
						|
        The user's discriminator. This is given when the username has conflicts.
 | 
						|
    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__ = ('_stored',)
 | 
						|
 | 
						|
    def __init__(self, *, state: ConnectionState, data: UserPayload) -> None:
 | 
						|
        super().__init__(state=state, data=data)
 | 
						|
        self._stored: bool = False
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return f'<User id={self.id} name={self.name!r} discriminator={self.discriminator!r} bot={self.bot}>'
 | 
						|
 | 
						|
    def __del__(self) -> None:
 | 
						|
        try:
 | 
						|
            if self._stored:
 | 
						|
                self._state.deref_user(self.id)
 | 
						|
        except Exception:
 | 
						|
            pass
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _copy(cls, user: User):
 | 
						|
        self = super()._copy(user)
 | 
						|
        self._stored = False
 | 
						|
        return self
 | 
						|
 | 
						|
    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)
 |