433 lines
13 KiB
Python
433 lines
13 KiB
Python
# SPDX-License-Identifier: MIT
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
|
|
|
|
if TYPE_CHECKING:
|
|
from aiohttp import ClientResponse, ClientWebSocketResponse
|
|
from requests import Response
|
|
|
|
from .client import SessionStartLimit
|
|
from .interactions import Interaction, ModalInteraction
|
|
|
|
_ResponseType = Union[ClientResponse, Response]
|
|
|
|
__all__ = (
|
|
"DiscordException",
|
|
"ClientException",
|
|
"NoMoreItems",
|
|
"GatewayNotFound",
|
|
"HTTPException",
|
|
"Forbidden",
|
|
"NotFound",
|
|
"DiscordServerError",
|
|
"InvalidData",
|
|
"WebhookTokenMissing",
|
|
"LoginFailure",
|
|
"SessionStartLimitReached",
|
|
"ConnectionClosed",
|
|
"PrivilegedIntentsRequired",
|
|
"InteractionException",
|
|
"InteractionTimedOut",
|
|
"InteractionResponded",
|
|
"InteractionNotResponded",
|
|
"ModalChainNotSupported",
|
|
"InteractionNotEditable",
|
|
"LocalizationKeyError",
|
|
)
|
|
|
|
|
|
class DiscordException(Exception):
|
|
"""Base exception class for disnake.
|
|
|
|
Ideally speaking, this could be caught to handle any exceptions raised from this library.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class ClientException(DiscordException):
|
|
"""Exception that's raised when an operation in the :class:`Client` fails.
|
|
|
|
These are usually for exceptions that happened due to user input.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class NoMoreItems(DiscordException):
|
|
"""Exception that is raised when an async iteration operation has no more items."""
|
|
|
|
pass
|
|
|
|
|
|
class GatewayNotFound(DiscordException):
|
|
"""An exception that is raised when the gateway for Discord could not be found"""
|
|
|
|
def __init__(self) -> None:
|
|
message = "The gateway to connect to Discord was not found."
|
|
super().__init__(message)
|
|
|
|
|
|
def _flatten_error_dict(d: Dict[str, Any], key: str = "") -> Dict[str, str]:
|
|
items: List[Tuple[str, str]] = []
|
|
for k, v in d.items():
|
|
new_key = f"{key}.{k}" if key else k
|
|
|
|
if isinstance(v, dict):
|
|
try:
|
|
_errors: List[Dict[str, Any]] = v["_errors"]
|
|
except KeyError:
|
|
items.extend(_flatten_error_dict(v, new_key).items())
|
|
else:
|
|
items.append((new_key, " ".join(x.get("message", "") for x in _errors)))
|
|
else:
|
|
items.append((new_key, v))
|
|
|
|
return dict(items)
|
|
|
|
|
|
class HTTPException(DiscordException):
|
|
"""Exception that's raised when an HTTP request operation fails.
|
|
|
|
Attributes
|
|
----------
|
|
response: :class:`aiohttp.ClientResponse`
|
|
The response of the failed HTTP request. This is an
|
|
instance of :class:`aiohttp.ClientResponse`. In some cases
|
|
this could also be a :class:`requests.Response`.
|
|
|
|
text: :class:`str`
|
|
The text of the error. Could be an empty string.
|
|
status: :class:`int`
|
|
The status code of the HTTP request.
|
|
code: :class:`int`
|
|
The Discord specific error code for the failure.
|
|
"""
|
|
|
|
def __init__(
|
|
self, response: _ResponseType, message: Optional[Union[str, Dict[str, Any]]]
|
|
) -> None:
|
|
self.response: _ResponseType = response
|
|
self.status: int = response.status # type: ignore
|
|
self.code: int
|
|
self.text: str
|
|
if isinstance(message, dict):
|
|
self.code = message.get("code", 0)
|
|
base = message.get("message", "")
|
|
errors = message.get("errors")
|
|
if errors:
|
|
errors = _flatten_error_dict(errors)
|
|
helpful = "\n".join(f"In {k}: {m}" for k, m in errors.items())
|
|
self.text = base + "\n" + helpful
|
|
else:
|
|
self.text = base
|
|
else:
|
|
self.text = message or ""
|
|
self.code = 0
|
|
|
|
fmt = "{0.status} {0.reason} (error code: {1})"
|
|
if len(self.text):
|
|
fmt += ": {2}"
|
|
|
|
super().__init__(fmt.format(self.response, self.code, self.text))
|
|
|
|
|
|
class Forbidden(HTTPException):
|
|
"""Exception that's raised for when status code 403 occurs.
|
|
|
|
Subclass of :exc:`HTTPException`.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class NotFound(HTTPException):
|
|
"""Exception that's raised for when status code 404 occurs.
|
|
|
|
Subclass of :exc:`HTTPException`.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class DiscordServerError(HTTPException):
|
|
"""Exception that's raised for when a 500 range status code occurs.
|
|
|
|
Subclass of :exc:`HTTPException`.
|
|
|
|
.. versionadded:: 1.5
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class InvalidData(ClientException):
|
|
"""Exception that's raised when the library encounters unknown
|
|
or invalid data from Discord.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class WebhookTokenMissing(DiscordException):
|
|
"""Exception that's raised when a :class:`Webhook` or :class:`SyncWebhook` is missing a token to make requests with.
|
|
|
|
.. versionadded:: 2.6
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class LoginFailure(ClientException):
|
|
"""Exception that's raised when the :meth:`Client.login` function
|
|
fails to log you in from improper credentials or some other misc.
|
|
failure.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class SessionStartLimitReached(ClientException):
|
|
"""Exception that's raised when :meth:`Client.connect` function
|
|
fails to connect to Discord due to the session start limit being reached.
|
|
|
|
.. versionadded:: 2.6
|
|
|
|
Attributes
|
|
----------
|
|
session_start_limit: :class:`.SessionStartLimit`
|
|
The current state of the session start limit.
|
|
|
|
"""
|
|
|
|
def __init__(self, session_start_limit: SessionStartLimit, requested: int = 1) -> None:
|
|
self.session_start_limit: SessionStartLimit = session_start_limit
|
|
super().__init__(
|
|
f"Daily session start limit has been reached, resets at {self.session_start_limit.reset_time} "
|
|
f"Requested {requested} shards, have only {session_start_limit.remaining} remaining."
|
|
)
|
|
|
|
|
|
class ConnectionClosed(ClientException):
|
|
"""Exception that's raised when the gateway connection is
|
|
closed for reasons that could not be handled internally.
|
|
|
|
Attributes
|
|
----------
|
|
code: :class:`int`
|
|
The close code of the websocket.
|
|
reason: :class:`str`
|
|
The reason provided for the closure.
|
|
shard_id: Optional[:class:`int`]
|
|
The shard ID that got closed if applicable.
|
|
"""
|
|
|
|
# https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes
|
|
GATEWAY_CLOSE_EVENT_REASONS: Dict[int, str] = {
|
|
4000: "Unknown error",
|
|
4001: "Unknown opcode",
|
|
4002: "Decode error",
|
|
4003: "Not authenticated",
|
|
4004: "Authentication failed",
|
|
4005: "Already authenticated",
|
|
4007: "Invalid sequence",
|
|
4008: "Rate limited",
|
|
4009: "Session timed out",
|
|
4010: "Invalid Shard",
|
|
4011: "Sharding required - you are required to shard your connection in order to connect.",
|
|
4012: "Invalid API version",
|
|
4013: "Invalid intents",
|
|
4014: "Disallowed intents - you tried to specify an intent that you have not enabled or are not approved for.",
|
|
}
|
|
|
|
# https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes
|
|
GATEWAY_VOICE_CLOSE_EVENT_REASONS: Dict[int, str] = {
|
|
**GATEWAY_CLOSE_EVENT_REASONS,
|
|
4002: "Failed to decode payload",
|
|
4006: "Session no longer valid",
|
|
4011: "Server not found",
|
|
4012: "Unknown protocol",
|
|
4014: "Disconnected, channel was deleted, you were kicked, voice server changed, or the main gateway session was dropped.",
|
|
4015: "Voice server crashed",
|
|
4016: "Unknown encryption mode",
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
socket: ClientWebSocketResponse,
|
|
*,
|
|
shard_id: Optional[int],
|
|
code: Optional[int] = None,
|
|
voice: bool = False,
|
|
) -> None:
|
|
# This exception is just the same exception except
|
|
# reconfigured to subclass ClientException for users
|
|
self.code: int = code or socket.close_code or -1
|
|
# aiohttp doesn't seem to consistently provide close reason
|
|
self.reason: str = self.GATEWAY_CLOSE_EVENT_REASONS.get(self.code, "Unknown reason")
|
|
if voice:
|
|
self.reason = self.GATEWAY_VOICE_CLOSE_EVENT_REASONS.get(self.code, "Unknown reason")
|
|
|
|
self.shard_id: Optional[int] = shard_id
|
|
super().__init__(
|
|
f"Shard ID {self.shard_id} WebSocket closed with {self.code}: {self.reason}"
|
|
)
|
|
|
|
|
|
class PrivilegedIntentsRequired(ClientException):
|
|
"""Exception that's raised when the gateway is requesting privileged intents
|
|
but they're not ticked in the developer page yet.
|
|
|
|
Go to https://discord.com/developers/applications/ and enable the intents
|
|
that are required. Currently these are as follows:
|
|
|
|
- :attr:`Intents.members`
|
|
- :attr:`Intents.presences`
|
|
- :attr:`Intents.message_content`
|
|
|
|
Attributes
|
|
----------
|
|
shard_id: Optional[:class:`int`]
|
|
The shard ID that got closed if applicable.
|
|
"""
|
|
|
|
def __init__(self, shard_id: Optional[int]) -> None:
|
|
self.shard_id: Optional[int] = shard_id
|
|
msg = (
|
|
f"Shard ID {shard_id} is requesting privileged intents that have not been explicitly enabled in the "
|
|
"developer portal. It is recommended to go to https://discord.com/developers/applications/ "
|
|
"and explicitly enable the privileged intents within your application's page. If this is not "
|
|
"possible, then consider disabling the privileged intents instead."
|
|
)
|
|
super().__init__(msg)
|
|
|
|
|
|
class InteractionException(ClientException):
|
|
"""Exception that's raised when an interaction operation fails
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
interaction: :class:`Interaction`
|
|
The interaction that was responded to.
|
|
"""
|
|
|
|
interaction: Interaction
|
|
|
|
|
|
class InteractionTimedOut(InteractionException):
|
|
"""Exception that's raised when an interaction takes more than 3 seconds
|
|
to respond but is not deferred.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
interaction: :class:`Interaction`
|
|
The interaction that was responded to.
|
|
"""
|
|
|
|
def __init__(self, interaction: Interaction) -> None:
|
|
self.interaction: Interaction = interaction
|
|
|
|
msg = (
|
|
"Interaction took more than 3 seconds to be responded to. "
|
|
'Please defer it using "interaction.response.defer" on the start of your command. '
|
|
"Later you may send a response by editing the deferred message "
|
|
'using "interaction.edit_original_response"'
|
|
"\n"
|
|
"Note: This might also be caused by a misconfiguration in the components "
|
|
"make sure you do not respond twice in case this is a component."
|
|
)
|
|
super().__init__(msg)
|
|
|
|
|
|
class InteractionResponded(InteractionException):
|
|
"""Exception that's raised when sending another interaction response using
|
|
:class:`InteractionResponse` when one has already been done before.
|
|
|
|
An interaction can only be responded to once.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
interaction: :class:`Interaction`
|
|
The interaction that's already been responded to.
|
|
"""
|
|
|
|
def __init__(self, interaction: Interaction) -> None:
|
|
self.interaction: Interaction = interaction
|
|
super().__init__("This interaction has already been responded to before")
|
|
|
|
|
|
class InteractionNotResponded(InteractionException):
|
|
"""Exception that's raised when editing an interaction response without
|
|
sending a response message first.
|
|
|
|
An interaction must be responded to exactly once.
|
|
|
|
.. versionadded:: 2.0
|
|
|
|
Attributes
|
|
----------
|
|
interaction: :class:`Interaction`
|
|
The interaction that hasn't been responded to.
|
|
"""
|
|
|
|
def __init__(self, interaction: Interaction) -> None:
|
|
self.interaction: Interaction = interaction
|
|
super().__init__("This interaction hasn't been responded to yet")
|
|
|
|
|
|
class ModalChainNotSupported(InteractionException):
|
|
"""Exception that's raised when responding to a modal with another modal.
|
|
|
|
.. versionadded:: 2.4
|
|
|
|
Attributes
|
|
----------
|
|
interaction: :class:`ModalInteraction`
|
|
The interaction that was responded to.
|
|
"""
|
|
|
|
def __init__(self, interaction: ModalInteraction) -> None:
|
|
self.interaction: ModalInteraction = interaction
|
|
super().__init__("You cannot respond to a modal with another modal.")
|
|
|
|
|
|
class InteractionNotEditable(InteractionException):
|
|
"""Exception that's raised when trying to use :func:`InteractionResponse.edit_message`
|
|
on an interaction without an associated message (which is thus non-editable).
|
|
|
|
.. versionadded:: 2.5
|
|
|
|
Attributes
|
|
----------
|
|
interaction: :class:`Interaction`
|
|
The interaction that was responded to.
|
|
"""
|
|
|
|
def __init__(self, interaction: Interaction) -> None:
|
|
self.interaction: Interaction = interaction
|
|
super().__init__("This interaction does not have a message to edit.")
|
|
|
|
|
|
class LocalizationKeyError(DiscordException):
|
|
"""Exception that's raised when a localization key lookup fails.
|
|
|
|
.. versionadded:: 2.5
|
|
|
|
Attributes
|
|
----------
|
|
key: :class:`str`
|
|
The localization key that couldn't be found.
|
|
"""
|
|
|
|
def __init__(self, key: str) -> None:
|
|
self.key: str = key
|
|
super().__init__(f"No localizations were found for the key '{key}'.")
|