aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--discord/client.py479
1 files changed, 365 insertions, 114 deletions
diff --git a/discord/client.py b/discord/client.py
index f036f678..4c16147c 100644
--- a/discord/client.py
+++ b/discord/client.py
@@ -106,6 +106,8 @@ class Client:
}
self._closed = False
+ # internals
+
def _resolve_mentions(self, content, mentions):
if isinstance(mentions, list):
return [user.id for user in mentions]
@@ -141,7 +143,6 @@ class Client:
else:
raise InvalidArgument('Destination must be Channel, PrivateChannel, User, or Object')
- # Compatibility shim
def __getattr__(self, name):
if name in ('user', 'email', 'servers', 'private_channels', 'messages'):
return getattr(self.connection, name)
@@ -149,7 +150,6 @@ class Client:
msg = "'{}' object has no attribute '{}'"
raise AttributeError(msg.format(self.__class__, name))
- # Compatibility shim
def __setattr__(self, name, value):
if name in ('user', 'email', 'servers', 'private_channels',
'messages'):
@@ -157,16 +157,6 @@ class Client:
else:
object.__setattr__(self, name, value)
- @property
- def is_logged_in(self):
- """bool: Indicates if the client has logged in successfully."""
- return self._is_logged_in
-
- @property
- def is_closed(self):
- """bool: Indicates if the websocket connection is closed."""
- return self._closed
-
@asyncio.coroutine
def _get_gateway(self):
resp = yield from self.session.get(endpoints.GATEWAY, headers=self.headers)
@@ -193,6 +183,103 @@ class Client:
if hasattr(self, method):
utils.create_task(self._run_event(method, *args, **kwargs), loop=self.loop)
+ @asyncio.coroutine
+ def keep_alive_handler(self, interval):
+ while not self._closed:
+ payload = {
+ 'op': 1,
+ 'd': int(time.time())
+ }
+
+ msg = 'Keeping websocket alive with timestamp {}'
+ log.debug(msg.format(payload['d']))
+ yield from self.ws.send(to_json(payload))
+ yield from asyncio.sleep(interval)
+
+ @asyncio.coroutine
+ def on_error(self, event_method, *args, **kwargs):
+ """|coro|
+
+ The default error handler provided by the client.
+
+ By default this prints to ``sys.stderr`` however it could be
+ overridden to have a different implementation.
+ Check :func:`discord.on_error` for more details.
+ """
+ print('Ignoring exception in {}'.format(event_method), file=sys.stderr)
+ traceback.print_exc()
+
+ def received_message(self, msg):
+ log.debug('WebSocket Event: {}'.format(msg))
+ self.dispatch('socket_response', msg)
+
+ op = msg.get('op')
+ data = msg.get('d')
+
+ if op != 0:
+ log.info('Unhandled op {}'.format(op))
+ return
+
+ event = msg.get('t')
+
+ if event == 'READY':
+ interval = data['heartbeat_interval'] / 1000.0
+ self.keep_alive = utils.create_task(self.keep_alive_handler(interval), loop=self.loop)
+
+ if event in ('READY', 'MESSAGE_CREATE', 'MESSAGE_DELETE',
+ 'MESSAGE_UPDATE', 'PRESENCE_UPDATE', 'USER_UPDATE',
+ 'CHANNEL_DELETE', 'CHANNEL_UPDATE', 'CHANNEL_CREATE',
+ 'GUILD_MEMBER_ADD', 'GUILD_MEMBER_REMOVE',
+ 'GUILD_MEMBER_UPDATE', 'GUILD_CREATE', 'GUILD_DELETE',
+ 'GUILD_ROLE_CREATE', 'GUILD_ROLE_DELETE', 'TYPING_START',
+ 'GUILD_ROLE_UPDATE', 'VOICE_STATE_UPDATE'):
+ parser = 'parse_' + event.lower()
+ if hasattr(self.connection, parser):
+ getattr(self.connection, parser)(data)
+ else:
+ log.info("Unhandled event {}".format(event))
+
+ @asyncio.coroutine
+ def _make_websocket(self):
+ if not self.is_logged_in:
+ raise ClientException('You must be logged in to connect')
+
+ self.gateway = yield from self._get_gateway()
+ self.ws = yield from websockets.connect(self.gateway)
+ self.ws.max_size = None
+ log.info('Created websocket connected to {0.gateway}'.format(self))
+ payload = {
+ 'op': 2,
+ 'd': {
+ 'token': self.token,
+ 'properties': {
+ '$os': sys.platform,
+ '$browser': 'discord.py',
+ '$device': 'discord.py',
+ '$referrer': '',
+ '$referring_domain': ''
+ },
+ 'v': 3
+ }
+ }
+
+ yield from self.ws.send(to_json(payload))
+ log.info('sent the initial payload to create the websocket')
+
+ # properties
+
+ @property
+ def is_logged_in(self):
+ """bool: Indicates if the client has logged in successfully."""
+ return self._is_logged_in
+
+ @property
+ def is_closed(self):
+ """bool: Indicates if the websocket connection is closed."""
+ return self._closed
+
+ # helpers/getters
+
def get_channel(self, id):
"""Returns a :class:`Channel` or :class:`PrivateChannel` with the following ID. If not found, returns None."""
return self.connection.get_channel(id)
@@ -231,18 +318,7 @@ class Client:
for member in server.members:
yield member
- @asyncio.coroutine
- def close(self):
- """Closes the websocket connection.
-
- To reconnect the websocket connection, :meth:`connect` must be used.
- """
- if self._closed:
- return
-
- yield from self.ws.close()
- self.keep_alive.cancel()
- self._closed = True
+ # login state management
@asyncio.coroutine
def login(self, email, password):
@@ -289,87 +365,14 @@ class Client:
self._is_logged_in = True
@asyncio.coroutine
- def keep_alive_handler(self, interval):
- while not self._closed:
- payload = {
- 'op': 1,
- 'd': int(time.time())
- }
-
- msg = 'Keeping websocket alive with timestamp {}'
- log.debug(msg.format(payload['d']))
- yield from self.ws.send(to_json(payload))
- yield from asyncio.sleep(interval)
-
- @asyncio.coroutine
- def on_error(self, event_method, *args, **kwargs):
+ def logout(self):
"""|coro|
- The default error handler provided by the client.
-
- By default this prints to ``sys.stderr`` however it could be
- overridden to have a different implementation.
- Check :func:`discord.on_error` for more details.
- """
- print('Ignoring exception in {}'.format(event_method), file=sys.stderr)
- traceback.print_exc()
-
- def received_message(self, msg):
- log.debug('WebSocket Event: {}'.format(msg))
- self.dispatch('socket_response', msg)
-
- op = msg.get('op')
- data = msg.get('d')
-
- if op != 0:
- log.info('Unhandled op {}'.format(op))
- return
-
- event = msg.get('t')
-
- if event == 'READY':
- interval = data['heartbeat_interval'] / 1000.0
- self.keep_alive = utils.create_task(self.keep_alive_handler(interval), loop=self.loop)
-
- if event in ('READY', 'MESSAGE_CREATE', 'MESSAGE_DELETE',
- 'MESSAGE_UPDATE', 'PRESENCE_UPDATE', 'USER_UPDATE',
- 'CHANNEL_DELETE', 'CHANNEL_UPDATE', 'CHANNEL_CREATE',
- 'GUILD_MEMBER_ADD', 'GUILD_MEMBER_REMOVE',
- 'GUILD_MEMBER_UPDATE', 'GUILD_CREATE', 'GUILD_DELETE',
- 'GUILD_ROLE_CREATE', 'GUILD_ROLE_DELETE', 'TYPING_START',
- 'GUILD_ROLE_UPDATE', 'VOICE_STATE_UPDATE'):
- parser = 'parse_' + event.lower()
- if hasattr(self.connection, parser):
- getattr(self.connection, parser)(data)
- else:
- log.info("Unhandled event {}".format(event))
-
- @asyncio.coroutine
- def _make_websocket(self):
- if not self.is_logged_in:
- raise ClientException('You must be logged in to connect')
-
- self.gateway = yield from self._get_gateway()
- self.ws = yield from websockets.connect(self.gateway)
- self.ws.max_size = None
- log.info('Created websocket connected to {0.gateway}'.format(self))
- payload = {
- 'op': 2,
- 'd': {
- 'token': self.token,
- 'properties': {
- '$os': sys.platform,
- '$browser': 'discord.py',
- '$device': 'discord.py',
- '$referrer': '',
- '$referring_domain': ''
- },
- 'v': 3
- }
- }
-
- yield from self.ws.send(to_json(payload))
- log.info('sent the initial payload to create the websocket')
+ Logs out of Discord and closes all connections."""
+ response = yield from self.session.post(endpoints.LOGOUT, headers=self.headers)
+ yield from self.close()
+ self._is_logged_in = False
+ log.debug(request_logging_format.format(method='POST', response=response))
@asyncio.coroutine
def connect(self):
@@ -396,6 +399,21 @@ class Client:
self.received_message(json.loads(msg))
+ @asyncio.coroutine
+ def close(self):
+ """Closes the websocket connection.
+
+ To reconnect the websocket connection, :meth:`connect` must be used.
+ """
+ if self._closed:
+ return
+
+ yield from self.ws.close()
+ self.keep_alive.cancel()
+ self._closed = True
+
+ # event registration
+
def event(self, coro):
"""A decorator that registers an event to listen to.
@@ -435,6 +453,8 @@ class Client:
return self.event(coro)
+ # Message sending/management
+
@asyncio.coroutine
def start_private_message(self, user):
"""|coro|
@@ -674,7 +694,7 @@ class Client:
yield from utils._verify_successful_response(response)
@asyncio.coroutine
- def edit_message(self, message, new_content, mentions=True):
+ def edit_message(self, message, new_content, *, mentions=True):
"""|coro|
Edits a :class:`Message` with the new message content.
@@ -718,16 +738,6 @@ class Client:
return Message(channel=channel, **data)
@asyncio.coroutine
- def logout(self):
- """|coro|
-
- Logs out of Discord and closes all connections."""
- response = yield from self.session.post(endpoints.LOGOUT, headers=self.headers)
- yield from self.close()
- self._is_logged_in = False
- log.debug(request_logging_format.format(method='POST', response=response))
-
- @asyncio.coroutine
def logs_from(self, channel, limit=100, *, before=None, after=None):
"""|coro|
@@ -789,3 +799,244 @@ class Client:
yield from utils._verify_successful_response(response)
messages = yield from response.json()
return generator_wrapper(messages)
+
+ # Member management
+
+ @asyncio.coroutine
+ def kick(self, member):
+ """|coro|
+
+ Kicks a :class:`Member` from the server they belong to.
+
+ Warning
+ --------
+ This function kicks the :class:`Member` based on the server it
+ belongs to, which is accessed via :attr:`Member.server`. So you
+ must have the proper permissions in that server.
+
+ Parameters
+ -----------
+ member : :class:`Member`
+ The member to kick from their server.
+
+ Raises
+ -------
+ Forbidden
+ You do not have the proper permissions to kick.
+ HTTPException
+ Kicking failed.
+ """
+
+ url = '{0}/{1.server.id}/members/{1.id}'.format(endpoints.SERVERS, member)
+ response = yield from self.session.delete(url, headers=self.headers)
+ log.debug(request_logging_format.format(method='DELETE', response=response))
+ yield from utils._verify_successful_response(response)
+
+ @asyncio.coroutine
+ def ban(self, member):
+ """|coro|
+
+ Bans a :class:`Member` from the server they belong to.
+
+ Warning
+ --------
+ This function bans the :class:`Member` based on the server it
+ belongs to, which is accessed via :attr:`Member.server`. So you
+ must have the proper permissions in that server.
+
+ Parameters
+ -----------
+ member : :class:`Member`
+ The member to ban from their server.
+
+ Raises
+ -------
+ Forbidden
+ You do not have the proper permissions to ban.
+ HTTPException
+ Banning failed.
+ """
+
+ url = '{0}/{1.server.id}/bans/{1.id}'.format(endpoints.SERVERS, member)
+ response = yield from self.session.put(url, headers=self.headers)
+ log.debug(request_logging_format.format(method='PUT', response=response))
+ yield from utils._verify_successful_response(response)
+
+ @asyncio.coroutine
+ def unban(self, member):
+ """|coro|
+
+ Unbans a :class:`Member` from the server they belong to.
+
+ Warning
+ --------
+ This function unbans the :class:`Member` based on the server it
+ belongs to, which is accessed via :attr:`Member.server`. So you
+ must have the proper permissions in that server.
+
+ Parameters
+ -----------
+ member : :class:`Member`
+ The member to unban from their server.
+
+ Raises
+ -------
+ Forbidden
+ You do not have the proper permissions to unban.
+ HTTPException
+ Unbanning failed.
+ """
+
+ url = '{0}/{1.server.id}/bans/{1.id}'.format(endpoints.SERVERS, member)
+ response = yield from self.session.delete(url, headers=self.headers)
+ log.debug(request_logging_format.format(method='DELETE', response=response))
+ yield from utils._verify_successful_response(response)
+
+ @asyncio.coroutine
+ def server_voice_state(self, member, *, mute=False, deafen=False):
+ """|coro|
+
+ Server mutes or deafens a specific :class:`Member`.
+
+ Warning
+ --------
+ This function mutes or un-deafens the :class:`Member` based on the
+ server it belongs to, which is accessed via :attr:`Member.server`.
+ So you must have the proper permissions in that server.
+
+ Parameters
+ -----------
+ member : :class:`Member`
+ The member to unban from their server.
+ mute : bool
+ Indicates if the member should be server muted or un-muted.
+ deafen : bool
+ Indicates if the member should be server deafened or un-deafened.
+
+ Raises
+ -------
+ Forbidden
+ You do not have the proper permissions to deafen or mute.
+ HTTPException
+ The operation failed.
+ """
+
+ url = '{0}/{1.server.id}/members/{1.id}'.format(endpoints.SERVERS, member)
+ payload = {
+ 'mute': mute,
+ 'deaf': deafen
+ }
+
+ response = yield from self.session.patch(url, headers=self.headers, data=to_json(payload))
+ log.debug(request_logging_format.format(method='PATCH', response=response))
+ yield from utils._verify_successful_response(response)
+
+ @asyncio.coroutine
+ def edit_profile(self, password, **fields):
+ """|coro|
+
+ Edits the current profile of the client.
+
+ All fields except ``password`` are optional.
+
+ 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.
+ 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.
+
+ Raises
+ ------
+ HTTPException
+ Editing your profile failed.
+ """
+
+ avatar_bytes = fields.get('avatar')
+ avatar = None
+ if avatar_bytes is not None:
+ fmt = 'data:{mime};base64,{data}'
+ mime = utils._get_mime_type_for_image(avatar_bytes)
+ b64 = b64encode(avatar_bytes).decode('ascii')
+ avatar = fmt.format(mime=mime, data=b64)
+
+ payload = {
+ 'password': password,
+ 'new_password': fields.get('new_password'),
+ 'email': fields.get('email', self.email),
+ 'username': fields.get('username', self.user.name),
+ 'avatar': avatar
+ }
+
+ url = '{0}/@me'.format(endpoints.USERS)
+ r = yield from self.session.patch(url, headers=self.headers, data=to_json(payload))
+ log.debug(request_logging_format.format(method='PATCH', response=r))
+ yield from utils._verify_successful_response(r)
+
+ data = yield from r.json()
+ log.debug(request_success_log.format(response=r, json=payload, data=data))
+ self.token = data['token']
+ self.email = data['email']
+ self.headers['authorization'] = self.token
+
+ @asyncio.coroutine
+ def change_status(self, game_id=None, idle=False):
+ """|coro|
+
+ Changes the client's status.
+
+ The game_id parameter is a numeric ID (not a string) that represents
+ a game being played currently. The list of game_id to actual games changes
+ constantly and would thus be out of date pretty quickly. An old version of
+ the game_id database can be seen `here`_ to help you get started.
+
+ The idle parameter is a boolean parameter that indicates whether the
+ client should go idle or not.
+
+ .. _here: https://gist.github.com/Rapptz/a82b82381b70a60c281b
+
+ Parameters
+ ----------
+ game_id : Optional[int]
+ The game ID being played. None if no game is being played.
+ idle : bool
+ Indicates if the client should go idle.
+
+ Raises
+ ------
+ InvalidArgument
+ If the ``game_id`` parameter is convertible integer or None.
+ """
+
+ idle_since = None if idle == False else int(time.time() * 1000)
+ try:
+ game_id = None if game_id is None else int(game_id)
+ except:
+ raise InvalidArgument('game_id must be convertible to an integer or None')
+
+ payload = {
+ 'op': 3,
+ 'd': {
+ 'game_id': game_id,
+ 'idle_since': idle_since
+ }
+ }
+
+ sent = to_json(payload)
+ log.debug('Sending "{}" to change status'.format(sent))
+ yield from self.ws.send(sent)