Browse Source

All HTTP requests now throw HTTPException.

pull/36/head
Rapptz 10 years ago
parent
commit
027b7b33c9
  1. 194
      discord/client.py
  2. 28
      discord/errors.py
  3. 9
      docs/api.rst

194
discord/client.py

@ -56,10 +56,11 @@ request_success_log = '{response.url} with {json} received {data}'
def _null_event(*args, **kwargs):
pass
def is_response_successful(response):
"""Helper function for checking if the status code is in the 200 range"""
def _verify_successful_response(response):
code = response.status_code
return code >= 200 and code < 300
success = code >= 200 and code < 300
if not success:
raise HTTPException(response)
class KeepAliveHandler(threading.Thread):
def __init__(self, seconds, socket, **kwargs):
@ -582,6 +583,8 @@ class Client(object):
Note that this method should rarely be called as :meth:`send_message` does it automatically.
This function raises :exc:`HTTPException` if the request failed.
:param user: A :class:`User` to start the private message with.
"""
if not isinstance(user, User):
@ -592,12 +595,11 @@ class Client(object):
}
r = requests.post('{}/{}/channels'.format(endpoints.USERS, self.user.id), json=payload, headers=self.headers)
if is_response_successful(r):
log.debug(request_logging_format.format(response=r))
_verify_successful_response(r)
data = r.json()
log.debug(request_success_log.format(response=r, json=payload, data=data))
self.private_channels.append(PrivateChannel(id=data['id'], user=user))
else:
log.error(request_logging_format.format(response=r))
def send_message(self, destination, content, mentions=True, tts=False):
"""Sends a message to the destination given with the content given.
@ -617,12 +619,13 @@ class Client(object):
no one is mentioned. Note that to mention someone in the content, you should use :meth:`User.mention`.
If the destination parameter is invalid, then this function raises :exc:`ClientException`.
This function raises :exc:`HTTPException` if the request failed.
:param destination: The location to send the message.
:param content: The content of the message to send.
:param mentions: A list of :class:`User` to mention in the message or a boolean. Ignored for private messages.
:param tts: If ``True``, sends tries to send the message using text-to-speech.
:return: The :class:`Message` sent or None if error occurred.
:return: The :class:`Message` sent.
"""
channel_id = self._resolve_destination(destination)
@ -640,14 +643,13 @@ class Client(object):
payload['tts'] = True
response = requests.post(url, json=payload, headers=self.headers)
if is_response_successful(response):
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
data = response.json()
log.debug(request_success_log.format(response=response, json=payload, data=data))
channel = self.get_channel(data.get('channel_id'))
message = Message(channel=channel, **data)
return message
else:
log.error(request_logging_format.format(response=response))
def send_file(self, destination, filename):
"""Sends a message to the destination given with the file given.
@ -655,10 +657,11 @@ class Client(object):
The destination parameter follows the same rules as :meth:`send_message`.
Note that this requires proper permissions in order to work.
This function raises :exc:`HTTPException` if the request failed.
:param destination: The location to send the message.
:param filename: The file to send.
:return: The :class:`Message` sent or None if an error occurred.
:return: The :class:`Message` sent.
"""
channel_id = self._resolve_destination(destination)
@ -672,14 +675,13 @@ class Client(object):
}
response = requests.post(url, files=files, headers=self.headers)
if is_response_successful(response):
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
data = response.json()
log.debug(request_success_log.format(response=response, json=response.text, data=filename))
channel = self.get_channel(data.get('channel_id'))
message = Message(channel=channel, **data)
return message
else:
log.error(request_logging_format.format(response=response))
def delete_message(self, message):
"""Deletes a :class:`Message`.
@ -687,24 +689,27 @@ class Client(object):
Your own messages could be deleted without any proper permissions. However to
delete other people's messages, you need the proper permissions to do so.
This function raises :exc:`HTTPException` if the request failed.
:param message: The :class:`Message` to delete.
:returns: True if the message was deleted successfully, False otherwise.
"""
url = '{}/{}/messages/{}'.format(endpoints.CHANNELS, message.channel.id, message.id)
response = requests.delete(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def edit_message(self, message, new_content, mentions=True):
"""Edits a :class:`Message` with the new message content.
The new_content must be able to be transformed into a string via ``str(new_content)``.
This function raises :exc:`HTTPException` if the request failed.
:param message: The :class:`Message` to edit.
:param new_content: The new content to replace the message with.
:param mentions: The mentions for the user. Same as :meth:`send_message`.
:return: The new edited message or None if an error occurred.
:return: The new edited message.
"""
channel = message.channel
@ -719,12 +724,11 @@ class Client(object):
payload['mentions'] = self._resolve_mentions(content, mentions)
response = requests.patch(url, headers=self.headers, json=payload)
if is_response_successful(response):
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
data = response.json()
log.debug(request_success_log.format(response=response, json=payload, data=data))
return Message(channel=channel, **data)
else:
log.error(request_logging_format.format(response=response))
def login(self, email, password):
"""Logs in the user with the following credentials and initialises
@ -746,8 +750,9 @@ class Client(object):
}
r = requests.post(endpoints.LOGIN, json=payload)
log.debug(request_logging_format.format(response=r))
_verify_successful_response(r)
if is_response_successful(r):
log.info('logging in returned status code {}'.format(r.status_code))
self.email = email
@ -756,12 +761,8 @@ class Client(object):
self.headers['authorization'] = self.token
gateway = requests.get(endpoints.GATEWAY, headers=self.headers)
if not is_response_successful(gateway):
raise GatewayNotFound()
self._create_websocket(gateway.json().get('url'), reconnect=False)
self._is_logged_in = True
else:
log.error(request_logging_format.format(response=r))
def register(self, username, invite, fingerprint=None):
"""Register a new unclaimed account using an invite to a server.
@ -771,7 +772,8 @@ class Client(object):
occur.
This function raises :exc:`GatewayNotFound` if the gateway to
connect the websocket is not found.
connect the websocket is not found. It also raises :exc:`HTTPException`
if the request failed.
:param str username: The username to register as.
:param invite: An invite URL or :class:`Invite` to register with.
@ -785,9 +787,10 @@ class Client(object):
}
r = requests.post(endpoints.REGISTER, json=payload)
log.debug(request_logging_format.format(response=r))
if r.status_code == 201:
log.info('register returned status code 200')
_verify_successful_response(r)
log.info('register returned a successful status code')
self.email = ''
body = r.json()
@ -799,8 +802,6 @@ class Client(object):
raise GatewayNotFound()
self._create_websocket(gateway.json().get('url'), reconnect=False)
self._is_logged_in = True
else:
log.error(request_logging_format.format(response=r))
def logout(self):
"""Logs out of Discord and closes all connections."""
@ -815,6 +816,8 @@ class Client(object):
Yielding from the generator returns a :class:`Message` object with the message data.
This function raises :exc:`HTTPException` if the request failed.
Example: ::
for message in client.logs_from(channel):
@ -832,13 +835,11 @@ class Client(object):
'limit': limit
}
response = requests.get(url, params=params, headers=self.headers)
if is_response_successful(response):
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
messages = response.json()
log.info('logs_from: {0.url} was successful'.format(response))
for message in messages:
yield Message(channel=channel, **message)
else:
log.error(request_logging_format.format(response=response))
def event(self, function):
"""A decorator that registers an event to listen to.
@ -862,50 +863,55 @@ class Client(object):
In order to delete the channel, the client must have the proper permissions
in the server the channel belongs to.
This function raises :exc:`HTTPException` if the request failed.
:param channel: The :class:`Channel` to delete.
:returns: True if channel was deleted successfully, False otherwise.
"""
url = '{}/{}'.format(endpoints.CHANNELS, channel.id)
response = requests.delete(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def kick(self, server, user):
"""Kicks a :class:`User` from their respective :class:`Server`.
You must have the proper permissions to kick a user in the server.
This function raises :exc:`HTTPException` if the request failed.
:param server: The :class:`Server` to kick the member from.
:param user: The :class:`User` to kick.
:returns: True if kick was successful, False otherwise.
"""
url = '{base}/{server}/members/{user}'.format(base=endpoints.SERVERS, server=server.id, user=user.id)
response = requests.delete(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def ban(self, server, user):
"""Bans a :class:`User` from their respective :class:`Server`.
You must have the proper permissions to ban a user in the server.
This function raises :exc:`HTTPException` if the request failed.
:param server: The :class:`Server` to ban the member from.
:param user: The :class:`User` to ban.
:returns: True if ban was successful, False otherwise.
"""
url = '{base}/{server}/bans/{user}'.format(base=endpoints.SERVERS, server=server.id, user=user.id)
response = requests.put(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def unban(self, server, name):
"""Unbans a :class:`User` from their respective :class:`Server`.
You must have the proper permissions to unban a user in the server.
This function raises :exc:`HTTPException` if the request failed.
:param server: The :class:`Server` to unban the member from.
:param user: The :class:`User` to unban.
:returns: True if unban was successful, False otherwise.
@ -914,18 +920,19 @@ class Client(object):
url = '{base}/{server}/bans/{user}'.format(base=endpoints.SERVERS, server=server.id, user=user.id)
response = requests.delete(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def edit_profile(self, password, **fields):
"""Edits the current profile of the client.
All fields except password are optional.
This function raises :exc:`HTTPException` if the request failed.
:param password: The current password for the client's account.
:param new_password: The new password you wish to change to.
:param email: The new email you wish to change to.
:param username: The new username you wish to change to.
:returns: True if profile edit was successful, False otherwise.
"""
payload = {
@ -938,18 +945,15 @@ class Client(object):
url = '{0}/@me'.format(endpoints.USERS)
response = requests.patch(url, headers=self.headers, json=payload)
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
if is_response_successful(response):
data = response.json()
log.debug(request_success_log.format(response=response, json=payload, data=data))
self.token = data['token']
self.email = data['email']
self.headers['authorization'] = self.token
self.user = User(**data)
return True
else:
log.debug(request_logging_format.format(response=response))
return False
def edit_channel(self, channel, **options):
"""Edits a :class:`Channel`.
@ -958,11 +962,12 @@ class Client(object):
References pointed to the channel will be updated with the new information.
This function raises :exc:`HTTPException` if the request failed.
:param channel: The :class:`Channel` to update.
:param name: The new channel name.
:param position: The new channel's position in the GUI.
:param topic: The new channel's topic.
:returns: True if editing was successful, False otherwise.
"""
url = '{0}/{1.id}'.format(endpoints.CHANNELS, channel)
@ -973,24 +978,24 @@ class Client(object):
}
response = requests.patch(url, headers=self.headers, json=payload)
if is_response_successful(response):
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
data = response.json()
log.debug(request_success_log.format(response=response, json=payload, data=data))
channel.update(server=channel.server, **data)
return True
else:
log.debug(request_logging_format.format(response=response))
return False
def create_channel(self, server, name, type='text'):
"""Creates a :class:`Channel` in the specified :class:`Server`.
Note that you need the proper permissions to create the channel.
This function raises :exc:`HTTPException` if the request failed.
:param server: The :class:`Server` to create the channel in.
:param name: The channel's name.
:param type: The type of channel to create. 'text' or 'voice'.
:returns: The newly created :class:`Channel` if successful, else None.
:returns: The newly created :class:`Channel`.
"""
payload = {
@ -1000,30 +1005,33 @@ class Client(object):
url = '{0}/{1.id}/channels'.format(endpoints.SERVERS, server)
response = requests.post(url, headers=self.headers, json=payload)
if is_response_successful(response):
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
data = response.json()
log.debug(request_success_log.format(response=response, data=data, json=payload))
channel = Channel(server=server, **data)
# We don't append it to server.channels because CHANNEL_CREATE handles it for us.
return channel
else:
log.debug(request_logging_format.format(response=response))
def leave_server(self, server):
"""Leaves a :class:`Server`.
This function raises :exc:`HTTPException` if the request failed.
:param server: The :class:`Server` to leave.
:returns: True if leaving was successful, False otherwise.
"""
url = '{0}/{1.id}'.format(endpoints.SERVERS, server)
response = requests.delete(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def create_invite(self, destination, **options):
"""Creates an invite for the destination which could be either a :class:`Server` or :class:`Channel`.
This function raises :exc:`HTTPException` if the request failed.
The available options are:
:param destination: The :class:`Server` or :class:`Channel` to create the invite to.
@ -1031,7 +1039,7 @@ class Client(object):
:param max_uses: How many uses the invite could be used for. If it's 0 then there are unlimited uses. Defaults to 0.
:param temporary: A boolean to denote that the invite grants temporary membership (i.e. they get kicked after they disconnect). Defaults to False.
:param xkcd: A boolean to indicate if the invite URL is human readable. Defaults to False.
:returns: The :class:`Invite` if creation is successful, None otherwise.
:returns: The :class:`Invite` if creation is successful.
"""
payload = {
@ -1043,38 +1051,42 @@ class Client(object):
url = '{0}/{1.id}/invites'.format(endpoints.CHANNELS, destination)
response = requests.post(url, headers=self.headers, json=payload)
if is_response_successful(response):
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
data = response.json()
log.debug(request_success_log.format(json=payload, response=response, data=data))
data['server'] = self.connection._get_server(data['guild']['id'])
channel_id = data['channel']['id']
data['channel'] = utils.find(lambda ch: ch.id == channel_id, data['server'].channels)
return Invite(**data)
else:
log.debug(request_logging_format.format(response=response))
def accept_invite(self, invite):
"""Accepts an :class:`Invite` or a URL to an invite.
The URL must be a discord.gg URL. e.g. "http://discord.gg/codehere"
This function raises :exc:`HTTPException` if the request failed. If
the invite is invalid, then :exc:`ClientException` is raised.
:param invite: The :class:`Invite` or URL to an invite to accept.
:returns: True if the invite was successfully accepted, False otherwise.
"""
destination = self._resolve_invite(invite)
if destination is None:
return False
raise ClientException('The invite ({}) is invalid.'.format(invite))
url = '{0}/invite/{1}'.format(endpoints.API_BASE, destination)
response = requests.post(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def edit_role(self, server, role, **fields):
"""Edits the specified :class:`Role` for the entire :class:`Server`.
This function raises :exc:`HTTPException` if the request failed.
.. versionchanged:: 0.8.0
Editing now uses keyword arguments instead of editing the :class:`Role` object directly.
@ -1091,8 +1103,6 @@ class Client(object):
:param permissions: The new :class:`Permissions` to change to. (optional)
:param colour: The new :class:`Colour` to change to. (optional) (aliased to color as well)
:param hoist: A boolean indicating if the role should be shown separately. (optional)
:return: ``True`` if editing was successful, ``False`` otherwise. If editing is successful,
the ``role`` parameter will be updated with the changes.
"""
url = '{0}/{1.id}/roles/{2.id}'.format(endpoints.SERVERS, server, role)
@ -1108,40 +1118,38 @@ class Client(object):
}
response = requests.patch(url, json=payload, headers=self.headers)
if is_response_successful(response):
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
data = response.json()
log.debug(request_success_log.format(json=payload, response=response, data=data))
role.update(**data)
return True
log.debug(request_logging_format.format(response=response))
return False
def delete_role(self, server, role):
"""Deletes the specified :class:`Role` for the entire :class:`Server`.
Works in a similar matter to :func:`edit_role`.
This function raises :exc:`HTTPException` if the request failed.
:param server: The :class:`Server` the role belongs to.
:param role: The :class:`Role` to delete.
:return: ``True`` if deleting was successful, ``False`` otherwise.
"""
url = '{0}/{1.id}/roles/{2.id}'.format(endpoints.SERVERS, server, role)
response = requests.delete(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def add_roles(self, member, *roles):
"""Gives the specified :class:`Member` a number of :class:`Role` s.
You must have the proper permissions to use this function.
This function raises :exc:`HTTPException` if the request failed.
This method **appends** a role to a member.
:param member: The :class:`Member` to give roles to.
:param roles: An iterable of :class:`Role` s to give the member.
:return: ``True`` if the operation was successful, ``False`` otherwise.
"""
url = '{0}/{1.server.id}/members/{1.id}'.format(endpoints.SERVERS, member)
@ -1152,16 +1160,16 @@ class Client(object):
response = requests.patch(url, headers=self.headers, json=payload)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def remove_roles(self, member, *roles):
"""Removes the :class:`Role` s from the :class:`Member`.
You must have the proper permissions to use this function.
This function raises :exc:`HTTPException` if the request failed.
:param member: The :class:`Member` to remove roles from.
:param roles: An iterable of :class:`Role` s to remove from the member.
:return: ``True`` if the operation was successful, ``False`` otherwise.
"""
url = '{0}/{1.server.id}/members/{1.id}'.format(endpoints.SERVERS, member)
@ -1177,7 +1185,7 @@ class Client(object):
response = requests.patch(url, headers=self.headers, json=payload)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def replace_roles(self, member, *roles):
"""Replaces the :class:`Member`'s roles.
@ -1189,9 +1197,10 @@ class Client(object):
call is ``client.replace_roles(member, d, e, c)`` then
the member has the roles ``[d, e, c]``.
This function raises :exc:`HTTPException` if the request failed.
:param member: The :class:`Member` to replace roles for.
:param roles: An iterable of :class:`Role` s to replace with.
:return: ``True`` if the operation was successful, ``False`` otherwise.
"""
url = '{0}/{1.server.id}/members/{1.id}'.format(endpoints.SERVERS, member)
@ -1202,25 +1211,25 @@ class Client(object):
response = requests.patch(url, headers=self.headers, json=payload)
log.debug(request_logging_format.format(response=response))
if is_response_successful(response):
member.roles = list(roles)
return True
_verify_successful_response(response)
return False
member.roles = list(roles)
def create_role(self, server, **fields):
"""Creates a :class:`Role`.
The fields parameter is the same as :func:`edit_role`.
:return: The :class:`Role` if creation was successful, None otherwise.
This function raises :exc:`HTTPException` if the request failed.
:return: The :class:`Role` that was created.
"""
url = '{0}/{1.id}/roles'.format(endpoints.SERVERS, server)
response = requests.post(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
_verify_successful_response(response)
if is_response_successful(response):
data = response.json()
everyone = server.id == data.get('id')
role = Role(everyone=everyone, **data)
@ -1229,8 +1238,6 @@ class Client(object):
# http request currently.
return role
return None
def set_channel_permissions(self, channel, target, allow=None, deny=None):
"""Sets the channel specific permission overwrites for a target in the
specified :class:`Channel`.
@ -1240,6 +1247,10 @@ class Client(object):
You must have the proper permissions to do this.
This function raises :exc:`HTTPException` if the request failed.
This function also raises ``TypeError`` if invalid arguments are
passed to this function.
Example code: ::
allow = discord.Permissions.none()
@ -1252,7 +1263,6 @@ class Client(object):
:param target: The :class:`Member` or :class:`Role` to overwrite permissions for.
:param allow: A :class:`Permissions` object representing the permissions to explicitly allow. (optional)
:param deny: A :class:`Permissions` object representing the permissions to explicitly deny. (optional)
:return: ``True`` if setting is successful, ``False`` otherwise.
"""
url = '{0}/{1.id}/permissions/{2.id}'.format(endpoints.CHANNELS, channel, target)
@ -1281,7 +1291,7 @@ class Client(object):
response = requests.put(url, json=payload, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def delete_channel_permissions(self, channel, target):
"""Removes a channel specific permission overwrites for a target
@ -1290,16 +1300,16 @@ class Client(object):
The target parameter follows the same rules as :meth:`set_channel_permissions`.
You must have the proper permissions to do this.
This function raises :exc:`HTTPException` if the request failed.
:param channel: The :class:`Channel` to give the specific permissions for.
:param target: The :class:`Member` or :class:`Role` to overwrite permissions for.
:return: ``True`` if deletion is successful, ``False`` otherwise.
"""
url = '{0}/{1.id}/permissions/{2.id}'.format(endpoints.CHANNELS, channel, target)
response = requests.delete(url, headers=self.headers)
log.debug(request_logging_format.format(response=response))
return is_response_successful(response)
_verify_successful_response(response)
def change_status(self, game_id=None, idle=False):
"""Changes the client's status.

28
discord/errors.py

@ -24,6 +24,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
try:
import http.client as httplib
except ImportError:
import httplib
class DiscordException(Exception):
"""Base exception class for discord.py
@ -39,7 +44,28 @@ class ClientException(DiscordException):
pass
class GatewayNotFound(DiscordException):
"""Thrown when the gateway hub for the :class:`Client` websocket is not found."""
"""An exception that is usually thrown when the gateway hub
for the :class:`Client` websocket is not found."""
def __init__(self):
message = 'The gateway to connect to discord was not found.'
super(GatewayNotFound, self).__init__(message)
class HTTPException(DiscordException):
"""Exception that's thrown when an HTTP request operation fails.
.. attribute:: response
The response of the failed HTTP request. This is an
instance of `requests.Response`__.
__ http://docs.python-requests.org/en/latest/api/#requests.Response
"""
def __init__(self, response, message=None):
self.response = response
if message is None:
message = httplib.responses.get(response.status_code, 'HTTP error')
message = '{0} (status code: {1.response.status_code}'.format(message, self)
super(HTTPException, self).__init__(message)

9
docs/api.rst

@ -282,9 +282,12 @@ Exceptions
The following exceptions are thrown by the library.
.. autoclass:: DiscordException
.. autoexception:: DiscordException
.. autoclass:: ClientException
.. autoexception:: ClientException
.. autoclass:: GatewayNotFound
.. autoexception:: HTTPException
:members:
.. autoexception:: GatewayNotFound

Loading…
Cancel
Save