From fa384f21148487bc188c83e829f1964b6d6e1b06 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Thu, 19 Jan 2017 19:37:11 -0500 Subject: [PATCH] Make ClientUser separate from a regular User. This removes Client.edit_profile in favour of ClientUser.edit. --- discord/__init__.py | 2 +- discord/client.py | 80 +------------ discord/member.py | 4 +- discord/state.py | 6 +- discord/user.py | 275 +++++++++++++++++++++++++++++++++----------- docs/api.rst | 7 ++ 6 files changed, 221 insertions(+), 153 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 10bbec8c8..f8a4eac2a 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -18,7 +18,7 @@ __copyright__ = 'Copyright 2015-2016 Rapptz' __version__ = '1.0.0a0' from .client import Client, AppInfo, ChannelPermissions -from .user import User +from .user import User, ClientUser from .game import Game from .emoji import Emoji, PartialEmoji from .channel import * diff --git a/discord/client.py b/discord/client.py index b967a56d9..725c0e77d 100644 --- a/discord/client.py +++ b/discord/client.py @@ -99,7 +99,7 @@ class Client: Attributes ----------- - user : Optional[:class:`User`] + user : Optional[:class:`ClientUser`] Represents the connected client. None if not logged in. voice_clients: List[:class:`VoiceClient`] Represents a list of voice connections. To connect to voice use @@ -814,84 +814,6 @@ class Client: yield from self.ws.send_as_json(payload) - @asyncio.coroutine - def edit_profile(self, password=None, **fields): - """|coro| - - Edits the current profile of the client. - - If a bot account is used then the password field is optional, - otherwise it is required. - - The :attr:`Client.user` object is not modified directly afterwards until the - corresponding WebSocket event is received. - - Note - ----- - To upload an avatar, a *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 *bytes-like object* is given through the use of ``fp.read()``. - - The only image formats supported for uploading is JPEG and PNG. - - Parameters - ----------- - password : str - The current password for the client's account. Not used - for bot accounts. - new_password : str - The new password you wish to change to. - email : str - The new email you wish to change to. - username :str - The new username you wish to change to. - avatar : bytes - A *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``. - ClientException - Password is required for non-bot accounts. - """ - - try: - avatar_bytes = fields['avatar'] - except KeyError: - avatar = self.user.avatar - else: - if avatar_bytes is not None: - avatar = utils._bytes_to_base64_data(avatar_bytes) - else: - avatar = None - - not_bot_account = not self.user.bot - if not_bot_account and password is None: - raise ClientException('Password is required for non-bot accounts.') - - args = { - 'password': password, - 'username': fields.get('username', self.user.name), - 'avatar': avatar - } - - if not_bot_account: - args['email'] = fields.get('email', self.email) - - if 'new_password' in fields: - args['new_password'] = fields['new_password'] - - data = yield from self.http.edit_profile(**args) - if not_bot_account: - self.email = data['email'] - if 'token' in data: - self.http._token(data['token'], bot=False) - @asyncio.coroutine def change_presence(self, *, game=None, status=None, afk=False): """|coro| diff --git a/discord/member.py b/discord/member.py index 97c9c254e..2cc5a92d7 100644 --- a/discord/member.py +++ b/discord/member.py @@ -29,7 +29,7 @@ import asyncio import discord.abc from . import utils -from .user import User +from .user import BaseUser from .game import Game from .permissions import Permissions from .enums import Status, ChannelType, try_enum @@ -74,7 +74,7 @@ class VoiceState: return ''.format(self) def flatten_user(cls): - for attr, value in User.__dict__.items(): + for attr, value in BaseUser.__dict__.items(): # ignore private/special methods if attr.startswith('_'): continue diff --git a/discord/state.py b/discord/state.py index 7f0c4eb91..f68656cc9 100644 --- a/discord/state.py +++ b/discord/state.py @@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE. """ from .guild import Guild -from .user import User +from .user import User, ClientUser from .game import Game from .emoji import Emoji, PartialEmoji from .reaction import Reaction @@ -239,7 +239,7 @@ class ConnectionState: def parse_ready(self, data): self._ready_state = ReadyState(launch=asyncio.Event(), guilds=[]) - self.user = self.store_user(data['user']) + self.user = ClientUser(state=self, data=data['user']) guilds = self._ready_state.guilds for guild_data in data['guilds']: @@ -339,7 +339,7 @@ class ConnectionState: self.dispatch('member_update', old_member, member) def parse_user_update(self, data): - self.user = User(state=self, data=data) + self.user = ClientUser(state=self, data=data) def parse_channel_delete(self, data): guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id')) diff --git a/discord/user.py b/discord/user.py index c08a3fb7a..7755c7c75 100644 --- a/discord/user.py +++ b/discord/user.py @@ -24,44 +24,15 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from .utils import snowflake_time +from .utils import snowflake_time, _bytes_to_base64_data from .enums import DefaultAvatar +from .errors import ClientException import discord.abc import asyncio -class User(discord.abc.Messageable): - """Represents a Discord user. - - Supported Operations: - - +-----------+---------------------------------------------+ - | Operation | Description | - +===========+=============================================+ - | x == y | Checks if two users are equal. | - +-----------+---------------------------------------------+ - | x != y | Checks if two users are not equal. | - +-----------+---------------------------------------------+ - | hash(x) | Return the user's hash. | - +-----------+---------------------------------------------+ - | str(x) | Returns the user's name with discriminator. | - +-----------+---------------------------------------------+ - - Attributes - ----------- - name: str - The user's username. - id: int - The user's unique ID. - discriminator: str - The user's discriminator. This is given when the username has conflicts. - avatar: str - The avatar hash the user has. Could be None. - bot: bool - Specifies if the user is a bot account. - """ - - __slots__ = ('name', 'id', 'discriminator', 'avatar', 'bot', '_state', '__weakref__') +class BaseUser: + __slots__ = ('name', 'id', 'discriminator', 'avatar', 'bot', '_state') def __init__(self, *, state, data): self._state = state @@ -75,7 +46,7 @@ class User(discord.abc.Messageable): return '{0.name}#{0.discriminator}'.format(self) def __eq__(self, other): - return isinstance(other, User) and other.id == self.id + return isinstance(other, BaseUser) and other.id == self.id def __ne__(self, other): return not self.__eq__(other) @@ -83,38 +54,6 @@ class User(discord.abc.Messageable): def __hash__(self): return self.id >> 22 - def __repr__(self): - return ''.format(self) - - @asyncio.coroutine - def _get_channel(self): - ch = yield from self.create_dm() - return ch - - @property - def dm_channel(self): - """Returns the :class:`DMChannel` 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) - - @asyncio.coroutine - def create_dm(self): - """Creates a :class:`DMChannel` with this user. - - This should be rarely called, as this is done transparently for most - people. - """ - found = self.dm_channel - if found is not None: - return found - - state = self._state - data = yield from state.http.start_private_message(self.id) - return state.add_dm_channel(data) - @property def avatar_url(self): """Returns a friendly URL version of the avatar the user has. @@ -191,7 +130,207 @@ class User(discord.abc.Messageable): if message.mention_everyone: return True - if self in message.mentions: - return True + for user in message.mentions: + if user.id == self.id: + return True return False + +class ClientUser(BaseUser): + """Represents your Discord user. + + Supported Operations: + + +-----------+---------------------------------------------+ + | Operation | Description | + +===========+=============================================+ + | x == y | Checks if two users are equal. | + +-----------+---------------------------------------------+ + | x != y | Checks if two users are not equal. | + +-----------+---------------------------------------------+ + | hash(x) | Return the user's hash. | + +-----------+---------------------------------------------+ + | str(x) | Returns the user's name with discriminator. | + +-----------+---------------------------------------------+ + + Attributes + ----------- + name: str + The user's username. + id: int + The user's unique ID. + discriminator: str + The user's discriminator. This is given when the username has conflicts. + avatar: str + The avatar hash the user has. Could be None. + bot: bool + Specifies if the user is a bot account. + verified: bool + Specifies if the user is a verified account. + email: Optional[str] + The email the user used when registering. + mfa_enabled: bool + Specifies if the user has MFA turned on and working. + """ + __slots__ = ('email', 'verified', 'mfa_enabled') + + def __init__(self, *, state, data): + super().__init__(state=state, data=data) + self.verified = data.get('verified', False) + self.email = data.get('email') + self.mfa_enabled = data.get('mfa_enabled', False) + + def __repr__(self): + return ''.format(self) + + + @asyncio.coroutine + def edit(self, **fields): + """|coro| + + Edits the current profile of the client. + + If a bot account is used then a password field is optional, + otherwise it is required. + + Note + ----- + To upload an avatar, a *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 *bytes-like object* is given through the use of ``fp.read()``. + + The only image formats supported for uploading is JPEG and PNG. + + Parameters + ----------- + password : str + The current password for the client's account. + Only applicable to user accounts. + new_password: str + The new password you wish to change to. + Only applicable to user accounts. + email: str + The new email you wish to change to. + Only applicable to user accounts. + username :str + The new username you wish to change to. + avatar: bytes + A *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``. + ClientException + Password is required for non-bot accounts. + """ + + try: + avatar_bytes = fields['avatar'] + except KeyError: + avatar = self.avatar + else: + if avatar_bytes is not None: + avatar = _bytes_to_base64_data(avatar_bytes) + else: + avatar = None + + not_bot_account = not self.bot + password = fields.get('password') + if not_bot_account and password is None: + raise ClientException('Password is required for non-bot accounts.') + + args = { + 'password': password, + 'username': fields.get('username', self.name), + 'avatar': avatar + } + + if not_bot_account: + args['email'] = fields.get('email', self.email) + + if 'new_password' in fields: + args['new_password'] = fields['new_password'] + + http = self._state.http + + data = yield from http.edit_profile(**args) + if not_bot_account: + self.email = data['email'] + try: + http._token(data['token'], bot=False) + except KeyError: + pass + + # manually update data by calling __init__ explicitly. + self.__init__(state=self._state, data=data) + +class User(BaseUser, discord.abc.Messageable): + """Represents a Discord user. + + Supported Operations: + + +-----------+---------------------------------------------+ + | Operation | Description | + +===========+=============================================+ + | x == y | Checks if two users are equal. | + +-----------+---------------------------------------------+ + | x != y | Checks if two users are not equal. | + +-----------+---------------------------------------------+ + | hash(x) | Return the user's hash. | + +-----------+---------------------------------------------+ + | str(x) | Returns the user's name with discriminator. | + +-----------+---------------------------------------------+ + + Attributes + ----------- + name: str + The user's username. + id: int + The user's unique ID. + discriminator: str + The user's discriminator. This is given when the username has conflicts. + avatar: str + The avatar hash the user has. Could be None. + bot: bool + Specifies if the user is a bot account. + """ + + __slots__ = ('__weakref__') + + def __repr__(self): + return ''.format(self) + + @asyncio.coroutine + def _get_channel(self): + ch = yield from self.create_dm() + return ch + + @property + def dm_channel(self): + """Returns the :class:`DMChannel` 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) + + @asyncio.coroutine + def create_dm(self): + """Creates a :class:`DMChannel` with this user. + + This should be rarely called, as this is done transparently for most + people. + """ + found = self.dm_channel + if found is not None: + return found + + state = self._state + data = yield from state.http.start_private_message(self.id) + return state.add_dm_channel(data) diff --git a/docs/api.rst b/docs/api.rst index 985afc3b0..7ab4c0b58 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -645,6 +645,13 @@ Object .. autoclass:: Object :members: +ClientUser +~~~~~~~~~~~~ + +.. autoclass:: ClientUser + :members: + :inherited-members: + User ~~~~~