Browse Source

Make ClientUser separate from a regular User.

This removes Client.edit_profile in favour of ClientUser.edit.
pull/468/head
Rapptz 8 years ago
parent
commit
fa384f2114
  1. 2
      discord/__init__.py
  2. 80
      discord/client.py
  3. 4
      discord/member.py
  4. 6
      discord/state.py
  5. 275
      discord/user.py
  6. 7
      docs/api.rst

2
discord/__init__.py

@ -18,7 +18,7 @@ __copyright__ = 'Copyright 2015-2016 Rapptz'
__version__ = '1.0.0a0' __version__ = '1.0.0a0'
from .client import Client, AppInfo, ChannelPermissions from .client import Client, AppInfo, ChannelPermissions
from .user import User from .user import User, ClientUser
from .game import Game from .game import Game
from .emoji import Emoji, PartialEmoji from .emoji import Emoji, PartialEmoji
from .channel import * from .channel import *

80
discord/client.py

@ -99,7 +99,7 @@ class Client:
Attributes Attributes
----------- -----------
user : Optional[:class:`User`] user : Optional[:class:`ClientUser`]
Represents the connected client. None if not logged in. Represents the connected client. None if not logged in.
voice_clients: List[:class:`VoiceClient`] voice_clients: List[:class:`VoiceClient`]
Represents a list of voice connections. To connect to voice use 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) 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 @asyncio.coroutine
def change_presence(self, *, game=None, status=None, afk=False): def change_presence(self, *, game=None, status=None, afk=False):
"""|coro| """|coro|

4
discord/member.py

@ -29,7 +29,7 @@ import asyncio
import discord.abc import discord.abc
from . import utils from . import utils
from .user import User from .user import BaseUser
from .game import Game from .game import Game
from .permissions import Permissions from .permissions import Permissions
from .enums import Status, ChannelType, try_enum from .enums import Status, ChannelType, try_enum
@ -74,7 +74,7 @@ class VoiceState:
return '<VoiceState self_mute={0.self_mute} self_deaf={0.self_deaf} channel={0.channel!r}>'.format(self) return '<VoiceState self_mute={0.self_mute} self_deaf={0.self_deaf} channel={0.channel!r}>'.format(self)
def flatten_user(cls): def flatten_user(cls):
for attr, value in User.__dict__.items(): for attr, value in BaseUser.__dict__.items():
# ignore private/special methods # ignore private/special methods
if attr.startswith('_'): if attr.startswith('_'):
continue continue

6
discord/state.py

@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
""" """
from .guild import Guild from .guild import Guild
from .user import User from .user import User, ClientUser
from .game import Game from .game import Game
from .emoji import Emoji, PartialEmoji from .emoji import Emoji, PartialEmoji
from .reaction import Reaction from .reaction import Reaction
@ -239,7 +239,7 @@ class ConnectionState:
def parse_ready(self, data): def parse_ready(self, data):
self._ready_state = ReadyState(launch=asyncio.Event(), guilds=[]) 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 guilds = self._ready_state.guilds
for guild_data in data['guilds']: for guild_data in data['guilds']:
@ -339,7 +339,7 @@ class ConnectionState:
self.dispatch('member_update', old_member, member) self.dispatch('member_update', old_member, member)
def parse_user_update(self, data): 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): def parse_channel_delete(self, data):
guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id')) guild = self._get_guild(utils._get_as_snowflake(data, 'guild_id'))

275
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. DEALINGS IN THE SOFTWARE.
""" """
from .utils import snowflake_time from .utils import snowflake_time, _bytes_to_base64_data
from .enums import DefaultAvatar from .enums import DefaultAvatar
from .errors import ClientException
import discord.abc import discord.abc
import asyncio import asyncio
class User(discord.abc.Messageable): class BaseUser:
"""Represents a Discord user. __slots__ = ('name', 'id', 'discriminator', 'avatar', 'bot', '_state')
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__')
def __init__(self, *, state, data): def __init__(self, *, state, data):
self._state = state self._state = state
@ -75,7 +46,7 @@ class User(discord.abc.Messageable):
return '{0.name}#{0.discriminator}'.format(self) return '{0.name}#{0.discriminator}'.format(self)
def __eq__(self, other): 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): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
@ -83,38 +54,6 @@ class User(discord.abc.Messageable):
def __hash__(self): def __hash__(self):
return self.id >> 22 return self.id >> 22
def __repr__(self):
return '<User id={0.id} name={0.name!r} discriminator={0.discriminator!r} bot={0.bot}>'.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 @property
def avatar_url(self): def avatar_url(self):
"""Returns a friendly URL version of the avatar the user has. """Returns a friendly URL version of the avatar the user has.
@ -191,7 +130,207 @@ class User(discord.abc.Messageable):
if message.mention_everyone: if message.mention_everyone:
return True return True
if self in message.mentions: for user in message.mentions:
return True if user.id == self.id:
return True
return False 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 '<ClientUser id={0.id} name={0.name!r} discriminator={0.discriminator!r}' \
' bot={0.bot} verified={0.verified} mfa_enabled={0.mfa_enabled}>'.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 '<User id={0.id} name={0.name!r} discriminator={0.discriminator!r} bot={0.bot}>'.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)

7
docs/api.rst

@ -645,6 +645,13 @@ Object
.. autoclass:: Object .. autoclass:: Object
:members: :members:
ClientUser
~~~~~~~~~~~~
.. autoclass:: ClientUser
:members:
:inherited-members:
User User
~~~~~ ~~~~~

Loading…
Cancel
Save