# -*- coding: utf-8 -*-
# cython: language_level=3
# Copyright (c) 2020 Nekokatt
# Copyright (c) 2021-present davfsa
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Exceptions and warnings that can be thrown by this library."""
from __future__ import annotations
__all__: typing.Sequence[str] = (
"HikariError",
"HikariWarning",
"HikariInterrupt",
"ComponentStateConflictError",
"UnrecognisedEntityError",
"NotFoundError",
"RateLimitTooLongError",
"UnauthorizedError",
"ForbiddenError",
"BadRequestError",
"HTTPError",
"HTTPResponseError",
"ClientHTTPResponseError",
"InternalServerError",
"ShardCloseCode",
"GatewayConnectionError",
"GatewayServerClosedConnectionError",
"GatewayError",
"MissingIntentWarning",
"MissingIntentError",
"BulkDeleteError",
"VoiceError",
)
import http
import json
import typing
import attrs
from hikari.internal import attrs_extensions
from hikari.internal import data_binding
from hikari.internal import enums
if typing.TYPE_CHECKING:
from hikari import intents as intents_
from hikari import messages
from hikari import snowflakes
from hikari.internal import routes
# The standard exceptions are all unsloted so slotting here would be a waste of time.
@attrs_extensions.with_copy
@attrs.define(auto_exc=True, repr=False, init=False, slots=False)
[docs]class HikariError(RuntimeError):
"""Base for an error raised by this API.
Any exceptions should derive from this.
.. note::
You should never initialize this exception directly.
"""
# The standard warnings are all unsloted so slotting here would be a waste of time.
@attrs_extensions.with_copy
@attrs.define(auto_exc=True, repr=False, init=False, slots=False)
[docs]class HikariWarning(RuntimeWarning):
"""Base for a warning raised by this API.
Any warnings should derive from this.
.. note::
You should never initialize this warning directly.
"""
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class HikariInterrupt(KeyboardInterrupt, HikariError):
"""Exception raised when a kill signal is handled internally."""
[docs] signum: int = attrs.field()
"""The signal number that was raised."""
[docs] signame: str = attrs.field()
"""The signal name that was raised."""
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class ComponentStateConflictError(HikariError):
"""Exception thrown when an action cannot be executed in the component's current state.
Dependent on context this will be thrown for components which are already
running or haven't been started yet.
"""
[docs] reason: str = attrs.field()
"""A string to explain the issue."""
def __str__(self) -> str:
return self.reason
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class UnrecognisedEntityError(HikariError):
"""An exception thrown when an unrecognised entity is found."""
[docs] reason: str = attrs.field()
"""A string to explain the issue."""
def __str__(self) -> str:
return self.reason
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class GatewayError(HikariError):
"""A base exception type for anything that can be thrown by the Gateway."""
[docs] reason: str = attrs.field()
"""A string to explain the issue."""
def __str__(self) -> str:
return self.reason
@typing.final
[docs]class ShardCloseCode(int, enums.Enum):
"""Reasons for a shard connection closure."""
NORMAL_CLOSURE = 1_000
GOING_AWAY = 1_001
PROTOCOL_ERROR = 1_002
TYPE_ERROR = 1_003
ENCODING_ERROR = 1_007
POLICY_VIOLATION = 1_008
TOO_BIG = 1_009
UNEXPECTED_CONDITION = 1_011
UNKNOWN_ERROR = 4_000
UNKNOWN_OPCODE = 4_001
DECODE_ERROR = 4_002
NOT_AUTHENTICATED = 4_003
AUTHENTICATION_FAILED = 4_004
ALREADY_AUTHENTICATED = 4_005
INVALID_SEQ = 4_007
RATE_LIMITED = 4_008
SESSION_TIMEOUT = 4_009
INVALID_SHARD = 4_010
SHARDING_REQUIRED = 4_011
INVALID_VERSION = 4_012
INVALID_INTENT = 4_013
DISALLOWED_INTENT = 4_014
@property
[docs] def is_standard(self) -> bool:
"""Return `True` if this is a standard code."""
return (self.value // 1000) == 1
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class GatewayConnectionError(GatewayError):
"""An exception thrown if a connection issue occurs."""
def __str__(self) -> str:
return f"Failed to connect to server: {self.reason!r}"
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class GatewayServerClosedConnectionError(GatewayError):
"""An exception raised when the server closes the connection."""
[docs] code: typing.Union[ShardCloseCode, int, None] = attrs.field(default=None)
"""Return the close code that was received, if there is one."""
[docs] can_reconnect: bool = attrs.field(default=False)
"""Return `True` if we can recover from this closure.
If `True`, it will try to reconnect after this is raised rather
than it being propagated to the caller. If `False`, this will
be raised, thus stopping the application unless handled explicitly by the
user.
"""
def __str__(self) -> str:
return f"Server closed connection with code {self.code} ({self.reason})"
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class HTTPError(HikariError):
"""Base exception raised if an HTTP error occurs while making a request."""
[docs] message: str = attrs.field()
"""The error message."""
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class HTTPResponseError(HTTPError):
"""Base exception for an erroneous HTTP response."""
[docs] url: str = attrs.field()
"""The URL that produced this error message."""
[docs] status: typing.Union[http.HTTPStatus, int] = attrs.field()
"""The HTTP status code for the response.
This will be `int` if it's outside the range of status codes in the HTTP
specification (e.g. one of Cloudflare's non-standard status codes).
"""
"""The headers received in the error response."""
[docs] raw_body: typing.Any = attrs.field()
"""The response body."""
[docs] message: str = attrs.field(default="")
"""The error message."""
[docs] code: int = attrs.field(default=0)
"""The error code."""
def __str__(self) -> str:
if isinstance(self.status, http.HTTPStatus):
name = self.status.name.replace("_", " ").title()
name_value = f"{name} {self.status.value}"
else:
name_value = f"Unknown Status {self.status}"
if self.code:
code_str = f" ({self.code})"
else:
code_str = ""
if self.message:
body = self.message
else:
try:
body = self.raw_body.decode("utf-8")
except (AttributeError, UnicodeDecodeError, TypeError, ValueError):
body = str(self.raw_body)
chomped = len(body) > 200
return f"{name_value}:{code_str} '{body[:200]}{'...' if chomped else ''}' for {self.url}"
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class ClientHTTPResponseError(HTTPResponseError):
"""Base exception for an erroneous HTTP response that is a client error.
All exceptions derived from this base should be treated as 4xx client
errors when encountered.
"""
def _dump_errors(obj: data_binding.JSONObject, obj_string: str = "") -> str:
string = ""
for key, value in obj.items():
if isinstance(value, typing.Sequence):
string += (obj_string or "root") + ":"
for item in value:
string += f"\n - {item['message']}"
string += "\n\n"
continue
current_obj_string = f"{obj_string}{'.' if obj_string else ''}{key}"
string += _dump_errors(value, current_obj_string)
return string
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class BadRequestError(ClientHTTPResponseError):
"""Raised when you send an invalid request somehow."""
[docs] status: http.HTTPStatus = attrs.field(default=http.HTTPStatus.BAD_REQUEST, init=False)
"""The HTTP status code for the response."""
[docs] errors: typing.Optional[typing.Mapping[str, data_binding.JSONObject]] = attrs.field(default=None, kw_only=True)
"""Dict of top level field names to field specific error paths.
For more information, this error format is loosely defined at
<https://discord.com/developers/docs/reference#error-messages> and is commonly
returned for 50035 errors.
"""
_cached_str: str = attrs.field(default=None, init=False)
def __str__(self) -> str:
if self._cached_str:
return self._cached_str
value = super().__str__()
if self.errors:
value += "\n\n"
try:
value += _dump_errors(self.errors).strip("\n")
except KeyError:
# Use the stdlib json.dumps here to be able to indent
value += json.dumps(self.errors, indent=2)
self._cached_str = value
return value
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class UnauthorizedError(ClientHTTPResponseError):
"""Raised when you are not authorized to access a specific resource."""
[docs] status: http.HTTPStatus = attrs.field(default=http.HTTPStatus.UNAUTHORIZED, init=False)
"""The HTTP status code for the response."""
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class ForbiddenError(ClientHTTPResponseError):
"""Raised when you are not allowed to access a specific resource.
This means you lack the permissions to do something, either because of
permissions set in a guild, or because your application is not whitelisted
to use a specific endpoint.
"""
[docs] status: http.HTTPStatus = attrs.field(default=http.HTTPStatus.FORBIDDEN, init=False)
"""The HTTP status code for the response."""
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class NotFoundError(ClientHTTPResponseError):
"""Raised when something is not found."""
[docs] status: http.HTTPStatus = attrs.field(default=http.HTTPStatus.NOT_FOUND, init=False)
"""The HTTP status code for the response."""
@attrs.define(auto_exc=True, kw_only=True, repr=False, slots=False)
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class InternalServerError(HTTPResponseError):
"""Base exception for an erroneous HTTP response that is a server error.
All exceptions derived from this base should be treated as 5xx server
errors when encountered. If you get one of these, it is not your fault!
"""
@attrs.define(auto_exc=True, repr=False, init=False, slots=False)
[docs]class MissingIntentWarning(HikariWarning):
"""Warning raised when subscribing to an event that cannot be fired.
This is caused by your application missing certain intents.
"""
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class BulkDeleteError(HikariError):
"""Exception raised when a bulk delete fails midway through a call.
This will contain the list of message items that failed to be deleted,
and will have a cause containing the initial exception.
"""
[docs] deleted_messages: snowflakes.SnowflakeishSequence[messages.PartialMessage] = attrs.field()
"""Any message objects that were deleted before an exception occurred."""
def __str__(self) -> str:
return f"Error encountered when bulk deleting messages ({len(self.deleted_messages)} messages deleted)"
@attrs.define(auto_exc=True, repr=False, init=False, slots=False)
[docs]class VoiceError(HikariError):
"""Error raised when a problem occurs with the voice subsystem."""
@attrs.define(auto_exc=True, repr=False, slots=False)
[docs]class MissingIntentError(HikariError, ValueError):
"""Error raised when you try to perform an action without an intent.
This is usually raised when querying the cache for something that is
unavailable due to certain intents being disabled.
"""
[docs] intents: intents_.Intents = attrs.field()
"""The combination of intents that are missing."""
def __str__(self) -> str:
return "You are missing the following intent(s): " + ", ".join(map(str, self.intents.split()))