clamor/exceptions.py
# -*- coding: utf-8 -*-
import logging
from enum import IntEnum
from itertools import starmap
from typing import Optional, Union
from asks.response_objects import Response
__all__ = (
'JSONErrorCode',
'ClamorError',
'RequestFailed',
'Unauthorized',
'Forbidden',
'NotFound',
'Hierarchied',
)
logger = logging.getLogger(__name__)
class JSONErrorCode(IntEnum):
"""Enum that holds the REST API JSON error codes."""
#: Unknown opcode.
UNKNOWN = 0
#: Unknown account.
UNKNOWN_ACCOUNT = 10001
#: Unknown application.
UNKNOWN_APPLICATION = 10002
#: Unknown channel.
UNKNOWN_CHANNEL = 10003
#: Unknown guild.
UNKNOWN_GUILD = 10004
#: Unknown integration.
UNKNOWN_INTEGRATION = 10005
#: Unknown invite.
UNKNOWN_INVITE = 10006
#: Unknown member.
UNKNOWN_MEMBER = 10007
#: Unknown message.
UNKNOWN_MESSAGE = 10008
#: Unknown overwrite.
UNKNOWN_OVERWRITE = 10009
#: Unknown provider.
UNKNOWN_PROVIDER = 10010
#: Unknown role.
UNKNOWN_ROLE = 10011
#: Unknown token.
UNKNOWN_TOKEN = 10012
#: Unknown user.
UNKNOWN_USER = 10013
#: Unknown emoji.
UNKNOWN_EMOJI = 10014
#: Unknown webhook.
UNKNOWN_WEBHOOK = 10015
#: Unknown ban.
UNKNOWN_BAN = 10026
#: Bots cannot use this endpoint.
BOTS_NOT_ALLOWED = 20001
#: Only bots can use this endpoint.
ONLY_BOTS_ALLOWED = 20002
#: Maximum number of guilds reached (100).
MAX_GUILDS_LIMIT = 30001
#: Maximum number of friends reached (1000).
MAX_FRIENDS_LIMIT = 30002
#: Maximum number of pinned messages reached (50).
MAX_PINS_LIMIT = 30003
#: Maximum number of recipients reached (10).
MAX_USERS_PER_DM = 30004
#: Maximum number of roles reached (250).
MAX_ROLES_LIMIT = 30005
#: Maximum number of reactions reached (20).
MAX_REACTIONS_LIMIT = 30010
#: Maximum number of guild channels reached (500).
MAX_GUILD_CHANNELS_LIMIT = 30013
#: Unauthorized.
UNAUTHORIZED = 40001
#: Missing access.
MISSING_ACCESS = 50001
#: Invalid account type.
INVALID_ACCOUNT_TYPE = 50002
#: Cannot execute action on DM channel.
INVALID_DM_ACTION = 50003
#: Widget disabled.
WIDGET_DISABLED = 50004
#: Cannot edit a message authored by another user.
CANNOT_EDIT = 50005
#: Cannot send an empty message.
EMPTY_MESSAGE = 50006
#: Cannot send messages to this user.
CANNOT_SEND_TO_USER = 50007
#: Cannot send messages in a voice channel.
CANNOT_SEND_IN_VC = 50008
#: Channel verification level is too high.
VERIFICATION_LEVEL_TOO_HIGH = 50009
#: OAuth2 application does not have a bot.
OAUTH_WITHOUT_BOT = 50010
#: OAuth2 application limit reached.
MAX_OAUTH_APPS = 50011
#: Invalid OAuth2 state.
INVALID_OAUTH_STATE = 50012
#: Missing permissions.
MISSING_PERMISSIONS = 50013
#: Invalid authentication token.
INVALID_TOKEN = 50014
#: Note is too long.
NOTE_TOO_LONG = 50015
#: Provided too few (at least 2) or too many (fewer than 100) messages to delete.
INVALID_BULK_DELETE = 50016
#: Invalid MFA level.
INVALID_MFA_LEVEL = 50017
#: Invalid password.
INVALID_PASSWORD = 50018
#: A message can only be pinned to the channel it was sent in.
INVALID_PIN = 50019
#: Invite code is either invalid or taken.
INVALID_VANITY_URL = 50020
#: Cannot execute action on a system message.
INVALID_MESSAGE_TARGET = 50021
#: Invalid OAuth2 access token.
INVALID_OAUTH_TOKEN = 50025
#: A message provided was too old to bulk delete.
TOO_OLD_TO_BULK_DELETE = 50034
#: Invalid form body.
INVALID_FORM_BODY = 50035
#: An invite was accepted to a guild the application's bot is not in.
INVALID_INVITE_GUILD = 50036
#: Invalid API version.
INVALID_API_VERSION = 50041
#: Reaction blocked.
REACTION_BLOCKED = 90001
#: Resource overloaded.
RESOURCE_OVERLOADED = 130000
@property
def name(self) -> str:
"""Returns a human-readable version of the enum member's name."""
return ' '.join(part.capitalize() for part in self._name_.split('_'))
class ClamorError(Exception):
"""Base exception class for any exceptions raised by this library.
Therefore, catching :class:`~clamor.exceptions.ClamorException` may
be used to handle **any** exceptions raised by this library.
"""
pass
class RequestFailed(ClamorError):
"""Exception that will be raised for failed HTTP requests to the REST API.
This extracts important components from the failed response
and presents them to the user in a readable way.
Parameters
----------
response : :class:`Response<asks:asks.response_objects.Response>`
The response for the failed request.
data : Union[dict, str], optional
The parsed response body.
Attributes
----------
response : :class:`Response<asks:asks.response_objects.Response>`
The response for the failed request.
status_code : int
The HTTP status code for the request.
bucket : Tuple[str, str]
A tuple containing request method and URL for debugging purposes.
error : :class:`~clamor.exceptions.JSONErrorCode`
The JSON error code returned by the API.
errors : dict
The unflattened JSON error dict.
message : str
The error message returned by the API.
"""
def __init__(self, response: Response, data: Optional[Union[dict, str]]):
self.response = response
self.status_code = response.status_code
self.bucket = (self.response.method.upper(), self.response.url)
self.error = None
self.errors = None
self.message = None
failed = 'Request to {0.bucket} failed with {0.error.value} {0.error.name}: {0.message}'
# Try to get any useful data from the dict
error_code = data.get('code', 0)
if isinstance(data, dict):
try:
self.error = JSONErrorCode(error_code)
except ValueError:
logger.warning('Unknown error code %d', error_code)
self.error = JSONErrorCode.UNKNOWN
self.errors = data.get('errors', {})
self.message = data.get('message', '')
else:
self.message = data
self.status_code = JSONErrorCode.UNKNOWN
if self.errors:
errors = self._flatten_errors(self.errors)
error_list = '\n'.join(
starmap('code: {1[0][code]}, message: {1[0][message]}'.format, errors.items()))
failed += '\nAdditional errors: {}'.format(error_list)
super().__init__(failed.format(self))
def _flatten_errors(self, errors: dict, key: str = '') -> dict:
messages = []
for k, v in errors.items():
if k == 'message':
continue
new_key = k
if key:
if key.isdigit():
new_key = '{}.{}'.format(key, k)
else:
new_key = '{}.[{}]'.format(key, k)
if isinstance(v, dict):
try:
_errors = v['_errors']
except KeyError:
messages.extend(self._flatten_errors(v, new_key).items())
else:
messages.append(
(new_key, ' '.join(error.get('message', '') for error in _errors)))
else:
messages.append((new_key, v))
return dict(messages)
class Unauthorized(RequestFailed):
"""Raised for HTTP status code ``401: Unauthorized``.
Essentially denoting that the user's token is wrong.
"""
pass
class Forbidden(RequestFailed):
"""Raised for HTTP status code ``403: Forbidden``.
Essentially denoting that your token is not permitted
to access a specific resource.
"""
pass
class NotFound(RequestFailed):
"""Raised for HTTP status code ``404: Not Found``.
Essentially denoting that the specified resource
does not exist.
"""
pass
class Hierarchied(ClamorError):
"""Raised when an action fails due to hierarchy.
This error is occurring when your bot tries to
edit someone with a higher role than their own
regardless of permissions.
Common examples:
----------------
- The bot is trying to edit the guild owner.
- The bot is trying to kick/ban members with
a higher role than their own.
*Even occurs if the bot has ``Kick/Ban Members`` permissions.*
"""
pass