# 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}'.")