From 1d3ec28aa5d8bd1b8cc6c52c15960e118b6a8cc8 Mon Sep 17 00:00:00 2001 From: fl0w <41456914+goverfl0w@users.noreply.github.com> Date: Mon, 11 Apr 2022 13:49:23 -0400 Subject: [PATCH 01/19] ADMIN/revert: remove unwanted file. This file was actually cloned outside of the client directory in a PR that had not appropriately adjusted to the new architectural layout of the library. --- interactions/bot.py | 1282 ------------------------------------------- 1 file changed, 1282 deletions(-) delete mode 100644 interactions/bot.py diff --git a/interactions/bot.py b/interactions/bot.py deleted file mode 100644 index 90c686bae..000000000 --- a/interactions/bot.py +++ /dev/null @@ -1,1282 +0,0 @@ -import re -import sys -from asyncio import get_event_loop, iscoroutinefunction -from functools import wraps -from importlib import import_module -from importlib.util import resolve_name -from inspect import getmembers -from logging import Logger -from types import ModuleType -from typing import Any, Callable, Coroutine, Dict, List, Optional, Union - -from .api.cache import Cache -from .api.cache import Item as Build -from .api.error import InteractionException, JSONException -from .api.gateway import WebSocketClient -from .api.http.client import HTTPClient -from .api.models.flags import Intents -from .api.models.guild import Guild -from .api.models.misc import MISSING, Snowflake -from .api.models.presence import ClientPresence -from .api.models.team import Application -from .base import get_logger -from .decor import command -from .decor import component as _component -from .enums import ApplicationCommandType, OptionType -from .models.command import ApplicationCommand, Option -from .models.component import Button, Modal, SelectMenu - -log: Logger = get_logger("client") -_token: str = "" # noqa -_cache: Optional[Cache] = None - - -class Client: - """ - A class representing the client connection to Discord's gateway and API via. WebSocket and HTTP. - - :ivar AbstractEventLoop _loop: The asynchronous event loop of the client. - :ivar HTTPClient _http: The user-facing HTTP connection to the Web API, as its own separate client. - :ivar WebSocketClient _websocket: An object-orientation of a websocket server connection to the Gateway. - :ivar Intents _intents: The Gateway intents of the application. Defaults to ``Intents.DEFAULT``. - :ivar Optional[List[Tuple[int]]] _shard: The list of bucketed shards for the application's connection. - :ivar Optional[ClientPresence] _presence: The RPC-like presence shown on an application once connected. - :ivar str _token: The token of the application used for authentication when connecting. - :ivar Optional[Dict[str, ModuleType]] _extensions: The "extensions" or cog equivalence registered to the main client. - :ivar Application me: The application representation of the client. - """ - - def __init__( - self, - token: str, - **kwargs, - ) -> None: - r""" - Establishes a client connection to the Web API and Gateway. - - :param token: The token of the application for authentication and connection. - :type token: str - :param \**kwargs: Multiple key-word arguments able to be passed through. - :type \**kwargs: dict - """ - - # Arguments - # ~~~~~~~~~ - # token : str - # The token of the application for authentication and connection. - # intents? : Optional[Intents] - # Allows specific control of permissions the application has when connected. - # In order to use multiple intents, the | operator is recommended. - # Defaults to ``Intents.DEFAULT``. - # shards? : Optional[List[Tuple[int]]] - # Dictates and controls the shards that the application connects under. - # presence? : Optional[ClientPresence] - # Sets an RPC-like presence on the application when connected to the Gateway. - # disable_sync? : Optional[bool] - # Controls whether synchronization in the user-facing API should be automatic or not. - - self._loop = get_event_loop() - self._http = HTTPClient(token=token) - self._intents = kwargs.get("intents", Intents.DEFAULT) - self._websocket = WebSocketClient(token=token, intents=self._intents) - self._shard = kwargs.get("shards", []) - self._presence = kwargs.get("presence") - self._token = token - self._extensions = {} - self._scopes = set([]) - self.me = None - _token = self._token # noqa: F841 - _cache = self._http.cache # noqa: F841 - - if kwargs.get("disable_sync"): - self._automate_sync = False - log.warning( - "Automatic synchronization has been disabled. Interactions may need to be manually synchronized." - ) - else: - self._automate_sync = True - - data = self._loop.run_until_complete(self._http.get_current_bot_information()) - self.me = Application(**data) - - @property - def guilds(self) -> List[Guild]: - """Returns a list of guilds the bot is in.""" - return [Guild(**_) for _ in self._http.cache.self_guilds.view] - - @property - def latency(self) -> float: - """Returns the connection latency in milliseconds.""" - - return self._websocket.latency * 1000 - - def start(self) -> None: - """Starts the client session.""" - self._loop.run_until_complete(self._ready()) - - def __register_events(self) -> None: - """Registers all raw gateway events to the known events.""" - self._websocket._dispatch.register(self.__raw_socket_create) - self._websocket._dispatch.register(self.__raw_channel_create, "on_channel_create") - self._websocket._dispatch.register(self.__raw_message_create, "on_message_create") - self._websocket._dispatch.register(self.__raw_guild_create, "on_guild_create") - - async def __compare_sync(self, data: dict, pool: List[dict]) -> bool: - """ - Compares an application command during the synchronization process. - - :param data: The application command to compare. - :type data: dict - :param pool: The "pool" or list of commands to compare from. - :type pool: List[dict] - :return: Whether the command has changed or not. - :rtype: bool - """ - attrs: List[str] = ["type", "name", "description", "options", "guild_id"] - log.info(f"Current attributes to compare: {', '.join(attrs)}.") - clean: bool = True - - for command in pool: - if command["name"] == data["name"]: - for attr in attrs: - if hasattr(data, attr) and command.get(attr) == data.get(attr): - continue - else: - clean = False - - return clean - - async def __create_sync(self, data: dict) -> None: - """ - Creates an application command during the synchronization process. - - :param data: The application command to create. - :type data: dict - """ - log.info(f"Creating command {data['name']}.") - - command: ApplicationCommand = ApplicationCommand( - **( - await self._http.create_application_command( - application_id=self.me.id, data=data, guild_id=data.get("guild_id") - ) - ) - ) - self._http.cache.interactions.add(Build(id=command.name, value=command)) - - async def __bulk_update_sync(self, data: List[dict], delete: Optional[bool] = False) -> None: - """ - Bulk updates a list of application commands during the synchronization process. - - The theory behind this is that instead of sending individual ``PATCH`` - requests to the Web API, we collect the commands needed and do a bulk - overwrite instead. This is to mitigate the amount of calls, and hopefully, - chances of hitting rate limits during the readying state. - - :param data: The application commands to update. - :type data: List[dict] - :param delete?: Whether these commands are being deleted or not. - :type delete: Optional[bool] - """ - guild_commands: dict = {} - global_commands: List[dict] = [] - - for command in data: - if command.get("guild_id"): - if guild_commands.get(command["guild_id"]): - guild_commands[command["guild_id"]].append(command) - else: - guild_commands[command["guild_id"]] = [command] - else: - global_commands.append(command) - - self._http.cache.interactions.add( - Build(id=command["name"], value=ApplicationCommand(**command)) - ) - - for guild, commands in guild_commands.items(): - log.info( - f"Guild commands {', '.join(command['name'] for command in commands)} under ID {guild} have been {'deleted' if delete else 'synced'}." - ) - await self._http.overwrite_application_command( - application_id=self.me.id, - data=[] if delete else commands, - guild_id=guild, - ) - - if global_commands: - log.info( - f"Global commands {', '.join(command['name'] for command in global_commands)} have been {'deleted' if delete else 'synced'}." - ) - await self._http.overwrite_application_command( - application_id=self.me.id, data=[] if delete else global_commands - ) - - async def _synchronize(self, payload: Optional[dict] = None) -> None: - """ - Synchronizes a command from the client-facing API to the Web API. - - :ivar payload?: The application command to synchronize. Defaults to ``None`` where a global synchronization process begins. - :type payload: Optional[dict] - """ - cache: Optional[List[dict]] = self._http.cache.interactions.view - - if cache: - log.info("A command cache was detected, using for synchronization instead.") - commands: List[dict] = cache - else: - log.info("No command cache was found present, retrieving from Web API instead.") - commands: Optional[Union[dict, List[dict]]] = await self._http.get_application_commands( - application_id=self.me.id, guild_id=payload.get("guild_id") if payload else None - ) - - # TODO: redo error handling. - if isinstance(commands, dict): - if commands.get("code"): # Error exists. - raise JSONException(commands["code"], message=f'{commands["message"]} |') - # TODO: redo error handling. - elif isinstance(commands, list): - for command in commands: - if command.get("code"): - # Error exists. - raise JSONException(command["code"], message=f'{command["message"]} |') - - names: List[str] = ( - [command["name"] for command in commands if command.get("name")] if commands else [] - ) - to_sync: list = [] - to_delete: list = [] - - if payload: - log.info(f"Checking command {payload['name']}.") - if payload["name"] in names: - if not await self.__compare_sync(payload, commands): - to_sync.append(payload) - else: - await self.__create_sync(payload) - else: - to_delete.extend(command for command in commands if command not in cache) - await self.__bulk_update_sync(to_sync) - await self.__bulk_update_sync(to_delete, delete=True) - - async def _ready(self) -> None: - """ - Prepares the client with an internal "ready" check to ensure - that all conditions have been met in a chronological order: - - .. code-block:: - - CLIENT START - |___ GATEWAY - | |___ READY - | |___ DISPATCH - |___ SYNCHRONIZE - | |___ CACHE - |___ DETECT DECORATOR - | |___ BUILD MODEL - | |___ SYNCHRONIZE - | |___ CALLBACK - LOOP - """ - ready: bool = False - - try: - if self.me.flags is not None: - # This can be None. - if self._intents.GUILD_PRESENCES in self._intents and not ( - self.me.flags.GATEWAY_PRESENCE in self.me.flags - or self.me.flags.GATEWAY_PRESENCE_LIMITED in self.me.flags - ): - raise RuntimeError("Client not authorised for the GUILD_PRESENCES intent.") - if self._intents.GUILD_MEMBERS in self._intents and not ( - self.me.flags.GATEWAY_GUILD_MEMBERS in self.me.flags - or self.me.flags.GATEWAY_GUILD_MEMBERS_LIMITED in self.me.flags - ): - raise RuntimeError("Client not authorised for the GUILD_MEMBERS intent.") - if self._intents.GUILD_MESSAGES in self._intents and not ( - self.me.flags.GATEWAY_MESSAGE_CONTENT in self.me.flags - or self.me.flags.GATEWAY_MESSAGE_CONTENT_LIMITED in self.me.flags - ): - log.critical("Client not authorised for the MESSAGE_CONTENT intent.") - elif self._intents.value != Intents.DEFAULT.value: - raise RuntimeError("Client not authorised for any privileged intents.") - - self.__register_events() - if self._automate_sync: - await self._synchronize() - ready = True - except Exception as error: - log.critical(f"Could not prepare the client: {error}") - finally: - if ready: - log.debug("Client is now ready.") - await self._login() - - async def _login(self) -> None: - """Makes a login with the Discord API.""" - while not self._websocket._closed: - await self._websocket._establish_connection(self._shard, self._presence) - - async def wait_until_ready(self) -> None: - """Helper method that waits until the websocket is ready.""" - await self._websocket.wait_until_ready() - - def event(self, coro: Coroutine, name: Optional[str] = MISSING) -> Callable[..., Any]: - """ - A decorator for listening to events dispatched from the - Gateway. - - :param coro: The coroutine of the event. - :type coro: Coroutine - :param name(?): The name of the event. If not given, this defaults to the coroutine's name. - :type name: Optional[str] - :return: A callable response. - :rtype: Callable[..., Any] - """ - self._websocket._dispatch.register(coro, name if name is not MISSING else coro.__name__) - return coro - - async def change_presence(self, presence: ClientPresence) -> None: - """ - A method that changes the current client's presence on runtime. - - .. note:: - There is a ratelimit to using this method (5 per minute). - As there's no gateway ratelimiter yet, breaking this ratelimit - will force your bot to disconnect. - - :param presence: The presence to change the bot to on identify. - :type presence: ClientPresence - """ - await self._websocket._update_presence(presence) - - def __check_command( - self, - command: ApplicationCommand, - coro: Coroutine, - regex: str = r"^[a-z0-9_-]{1,32}$", - ) -> None: - """ - Checks if a command is valid. - """ - reg = re.compile(regex) - _options_names: List[str] = [] - _sub_groups_present: bool = False - _sub_cmds_present: bool = False - - def __check_sub_group(_sub_group: Option): - nonlocal _sub_groups_present - _sub_groups_present = True - if _sub_group.name is MISSING: - raise InteractionException(11, message="Sub command groups must have a name.") - __indent = 4 - log.debug( - f"{' ' * __indent}checking sub command group '{_sub_group.name}' of command '{command.name}'" - ) - if not re.fullmatch(reg, _sub_group.name): - raise InteractionException( - 11, - message=f"The sub command group name does not match the regex for valid names ('{regex}')", - ) - elif _sub_group.description is MISSING and not _sub_group.description: - raise InteractionException(11, message="A description is required.") - elif len(_sub_group.description) > 100: - raise InteractionException( - 11, message="Descriptions must be less than 100 characters." - ) - if not _sub_group.options: - raise InteractionException(11, message="sub command groups must have subcommands!") - if len(_sub_group.options) > 25: - raise InteractionException( - 11, message="A sub command group cannot contain more than 25 sub commands!" - ) - for _sub_command in _sub_group.options: - __check_sub_command(Option(**_sub_command), _sub_group) - - def __check_sub_command(_sub_command: Option, _sub_group: Option = MISSING): - nonlocal _sub_cmds_present - _sub_cmds_present = True - if _sub_command.name is MISSING: - raise InteractionException(11, message="sub commands must have a name!") - if _sub_group is not MISSING: - __indent = 8 - log.debug( - f"{' ' * __indent}checking sub command '{_sub_command.name}' of group '{_sub_group.name}'" - ) - else: - __indent = 4 - log.debug( - f"{' ' * __indent}checking sub command '{_sub_command.name}' of command '{command.name}'" - ) - if not re.fullmatch(reg, _sub_command.name): - raise InteractionException( - 11, - message=f"The sub command name does not match the regex for valid names ('{reg}')", - ) - elif _sub_command.description is MISSING or not _sub_command.description: - raise InteractionException(11, message="A description is required.") - elif len(_sub_command.description) > 100: - raise InteractionException( - 11, message="Descriptions must be less than 100 characters." - ) - if _sub_command.options is not MISSING and _sub_command.options: - if len(_sub_command.options) > 25: - raise InteractionException( - 11, message="Your sub command must have less than 25 options." - ) - _sub_opt_names = [] - for _opt in _sub_command.options: - __check_options(Option(**_opt), _sub_opt_names, _sub_command) - del _sub_opt_names - - def __check_options(_option: Option, _names: list, _sub_command: Option = MISSING): - nonlocal _options_names - if getattr(_option, "autocomplete", False) and getattr(_option, "choices", False): - log.warning("Autocomplete may not be set to true if choices are present.") - if _option.name is MISSING: - raise InteractionException(11, message="Options must have a name.") - if _sub_command is not MISSING: - __indent = 8 if not _sub_groups_present else 12 - log.debug( - f"{' ' * __indent}checking option '{_option.name}' of sub command '{_sub_command.name}'" - ) - else: - __indent = 4 - log.debug( - f"{' ' * __indent}checking option '{_option.name}' of command '{command.name}'" - ) - _options_names.append(_option.name) - if not re.fullmatch(reg, _option.name): - raise InteractionException( - 11, - message=f"The option name does not match the regex for valid names ('{regex}')", - ) - if _option.description is MISSING or not _option.description: - raise InteractionException( - 11, - message="A description is required.", - ) - elif len(_option.description) > 100: - raise InteractionException( - 11, - message="Descriptions must be less than 100 characters.", - ) - if _option.name in _names: - raise InteractionException( - 11, message="You must not have two options with the same name in a command!" - ) - _names.append(_option.name) - - def __check_coro(): - __indent = 4 - log.debug(f"{' ' * __indent}Checking coroutine: '{coro.__name__}'") - if not len(coro.__code__.co_varnames): - raise InteractionException( - 11, message="Your command needs at least one argument to return context." - ) - elif "kwargs" in coro.__code__.co_varnames: - return - elif _sub_cmds_present and len(coro.__code__.co_varnames) < 2: - raise InteractionException( - 11, message="Your command needs one argument for the sub_command." - ) - elif _sub_groups_present and len(coro.__code__.co_varnames) < 3: - raise InteractionException( - 11, - message="Your command needs one argument for the sub_command and one for the sub_command_group.", - ) - add: int = 1 + abs(_sub_cmds_present) + abs(_sub_groups_present) - - if len(coro.__code__.co_varnames) - add < len(set(_options_names)): - log.debug( - "Coroutine is missing arguments for options:" - f" {[_arg for _arg in _options_names if _arg not in coro.__code__.co_varnames]}" - ) - raise InteractionException( - 11, message="You need one argument for every option name in your command!" - ) - - if command.name is MISSING: - raise InteractionException(11, message="Your command must have a name.") - - else: - log.debug(f"checking command '{command.name}':") - if ( - not re.fullmatch(reg, command.name) - and command.type == ApplicationCommandType.CHAT_INPUT - ): - raise InteractionException( - 11, message=f"Your command does not match the regex for valid names ('{regex}')" - ) - elif command.type == ApplicationCommandType.CHAT_INPUT and ( - command.description is MISSING or not command.description - ): - raise InteractionException(11, message="A description is required.") - elif command.type != ApplicationCommandType.CHAT_INPUT and ( - command.description is not MISSING and command.description - ): - raise InteractionException( - 11, message="Only chat-input commands can have a description." - ) - - elif command.description is not MISSING and len(command.description) > 100: - raise InteractionException(11, message="Descriptions must be less than 100 characters.") - - if command.options and command.options is not MISSING: - if len(command.options) > 25: - raise InteractionException( - 11, message="Your command must have less than 25 options." - ) - - if command.type != ApplicationCommandType.CHAT_INPUT: - raise InteractionException( - 11, message="Only CHAT_INPUT commands can have options/sub-commands!" - ) - - _opt_names = [] - for _option in command.options: - if _option.type == OptionType.SUB_COMMAND_GROUP: - __check_sub_group(_option) - - elif _option.type == OptionType.SUB_COMMAND: - __check_sub_command(_option) - - else: - __check_options(_option, _opt_names) - del _opt_names - - __check_coro() - - def command( - self, - *, - type: Optional[Union[int, ApplicationCommandType]] = ApplicationCommandType.CHAT_INPUT, - name: Optional[str] = MISSING, - description: Optional[str] = MISSING, - scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING, - options: Optional[ - Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]] - ] = MISSING, - default_permission: Optional[bool] = MISSING, - ) -> Callable[..., Any]: - """ - A decorator for registering an application command to the Discord API, - as well as being able to listen for ``INTERACTION_CREATE`` dispatched - gateway events. - - The structure of a chat-input command: - - .. code-block:: python - - @command(name="command-name", description="this is a command.") - async def command_name(ctx): - ... - - You are also able to establish it as a message or user command by simply passing - the ``type`` kwarg field into the decorator: - - .. code-block:: python - - @command(type=interactions.ApplicationCommandType.MESSAGE, name="Message Command") - async def message_command(ctx): - ... - - The ``scope`` kwarg field may also be used to designate the command in question - applicable to a guild or set of guilds. - - :param type?: The type of application command. Defaults to :meth:`interactions.enums.ApplicationCommandType.CHAT_INPUT` or ``1``. - :type type: Optional[Union[str, int, ApplicationCommandType]] - :param name: The name of the application command. This *is* required but kept optional to follow kwarg rules. - :type name: Optional[str] - :param description?: The description of the application command. This should be left blank if you are not using ``CHAT_INPUT``. - :type description: Optional[str] - :param scope?: The "scope"/applicable guilds the application command applies to. - :type scope: Optional[Union[int, Guild, List[int], List[Guild]]] - :param options?: The "arguments"/options of an application command. This should be left blank if you are not using ``CHAT_INPUT``. - :type options: Optional[Union[Dict[str, Any], List[Dict[str, Any]], Option, List[Option]]] - :param default_permission?: The default permission of accessibility for the application command. Defaults to ``True``. - :type default_permission: Optional[bool] - :return: A callable response. - :rtype: Callable[..., Any] - """ - - def decorator(coro: Coroutine) -> Callable[..., Any]: - - commands: List[ApplicationCommand] = command( - type=type, - name=name, - description=description, - scope=scope, - options=options, - default_permission=default_permission, - ) - self.__check_command(command=ApplicationCommand(**commands[0]), coro=coro) - - if self._automate_sync: - if self._loop.is_running(): - [self._loop.create_task(self._synchronize(command)) for command in commands] - else: - [ - self._loop.run_until_complete(self._synchronize(command)) - for command in commands - ] - - if scope is not MISSING: - if isinstance(scope, List): - [self._scopes.add(_ if isinstance(_, int) else _.id) for _ in scope] - else: - self._scopes.add(scope if isinstance(scope, int) else scope.id) - - return self.event(coro, name=f"command_{name}") - - return decorator - - def message_command( - self, - *, - name: str, - scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING, - default_permission: Optional[bool] = MISSING, - ) -> Callable[..., Any]: - """ - A decorator for registering a message context menu to the Discord API, - as well as being able to listen for ``INTERACTION_CREATE`` dispatched - gateway events. - - The structure of a message context menu: - - .. code-block:: python - - @message_command(name="Context menu name") - async def context_menu_name(ctx): - ... - - The ``scope`` kwarg field may also be used to designate the command in question - applicable to a guild or set of guilds. - - :param name: The name of the application command. - :type name: Optional[str] - :param scope?: The "scope"/applicable guilds the application command applies to. Defaults to ``None``. - :type scope: Optional[Union[int, Guild, List[int], List[Guild]]] - :param default_permission?: The default permission of accessibility for the application command. Defaults to ``True``. - :type default_permission: Optional[bool] - :return: A callable response. - :rtype: Callable[..., Any] - """ - - def decorator(coro: Coroutine) -> Callable[..., Any]: - - commands: List[ApplicationCommand] = command( - type=ApplicationCommandType.MESSAGE, - name=name, - scope=scope, - default_permission=default_permission, - ) - self.__check_command(ApplicationCommand(**commands[0]), coro) - - if self._automate_sync: - if self._loop.is_running(): - [self._loop.create_task(self._synchronize(command)) for command in commands] - else: - [ - self._loop.run_until_complete(self._synchronize(command)) - for command in commands - ] - - return self.event(coro, name=f"command_{name}") - - return decorator - - def user_command( - self, - *, - name: str, - scope: Optional[Union[int, Guild, List[int], List[Guild]]] = MISSING, - default_permission: Optional[bool] = MISSING, - ) -> Callable[..., Any]: - """ - A decorator for registering a user context menu to the Discord API, - as well as being able to listen for ``INTERACTION_CREATE`` dispatched - gateway events. - - The structure of a user context menu: - - .. code-block:: python - - @user_command(name="Context menu name") - async def context_menu_name(ctx): - ... - - The ``scope`` kwarg field may also be used to designate the command in question - applicable to a guild or set of guilds. - - :param name: The name of the application command. - :type name: Optional[str] - :param scope?: The "scope"/applicable guilds the application command applies to. Defaults to ``None``. - :type scope: Optional[Union[int, Guild, List[int], List[Guild]]] - :param default_permission?: The default permission of accessibility for the application command. Defaults to ``True``. - :type default_permission: Optional[bool] - :return: A callable response. - :rtype: Callable[..., Any] - """ - - def decorator(coro: Coroutine) -> Callable[..., Any]: - - commands: List[ApplicationCommand] = command( - type=ApplicationCommandType.USER, - name=name, - scope=scope, - default_permission=default_permission, - ) - - self.__check_command(ApplicationCommand(**commands[0]), coro) - - if self._automate_sync: - if self._loop.is_running(): - [self._loop.create_task(self._synchronize(command)) for command in commands] - else: - [ - self._loop.run_until_complete(self._synchronize(command)) - for command in commands - ] - - return self.event(coro, name=f"command_{name}") - - return decorator - - def component(self, component: Union[str, Button, SelectMenu]) -> Callable[..., Any]: - """ - A decorator for listening to ``INTERACTION_CREATE`` dispatched gateway - events involving components. - - The structure for a component callback: - - .. code-block:: python - - # Method 1 - @component(interactions.Button( - style=interactions.ButtonStyle.PRIMARY, - label="click me!", - custom_id="click_me_button", - )) - async def button_response(ctx): - ... - - # Method 2 - @component("custom_id") - async def button_response(ctx): - ... - - The context of the component callback decorator inherits the same - as of the command decorator. - - :param component: The component you wish to callback for. - :type component: Union[str, Button, SelectMenu] - :return: A callable response. - :rtype: Callable[..., Any] - """ - - def decorator(coro: Coroutine) -> Any: - payload: str = ( - _component(component).custom_id - if isinstance(component, (Button, SelectMenu)) - else component - ) - return self.event(coro, name=f"component_{payload}") - - return decorator - - @staticmethod - def _find_command(commands: List[Dict], command: str) -> ApplicationCommand: - """ - Iterates over `commands` and returns an :class:`ApplicationCommand` if it matches the name from `command` - - :ivar commands: The list of dicts to iterate through - :type commands: List[Dict] - :ivar command: The name of the command to match: - :type command: str - :return: An ApplicationCommand model - :rtype: ApplicationCommand - """ - _command: Dict - _command_obj = next( - ( - ApplicationCommand(**_command) - for _command in commands - if _command["name"] == command - ), - None, - ) - - if not _command_obj or (hasattr(_command_obj, "id") and not _command_obj.id): - raise InteractionException( - 6, - message="The command does not exist. Make sure to define" - + " your autocomplete callback after your commands", - ) - else: - return _command_obj - - def autocomplete( - self, command: Union[ApplicationCommand, int, str, Snowflake], name: str - ) -> Callable[..., Any]: - """ - A decorator for listening to ``INTERACTION_CREATE`` dispatched gateway - events involving autocompletion fields. - - The structure for an autocomplete callback: - - .. code-block:: python - - @autocomplete(command="command_name", name="option_name") - async def autocomplete_choice_list(ctx, user_input: str = ""): - await ctx.populate([ - interactions.Choice(...), - interactions.Choice(...), - ... - ]) - - :param command: The command, command ID, or command name with the option. - :type command: Union[ApplicationCommand, int, str, Snowflake] - :param name: The name of the option to autocomplete. - :type name: str - :return: A callable response. - :rtype: Callable[..., Any] - """ - - if isinstance(command, ApplicationCommand): - _command: Union[Snowflake, int] = command.id - elif isinstance(command, str): - _command_obj: ApplicationCommand = self._http.cache.interactions.get(command) - if not _command_obj or not _command_obj.id: - if getattr(_command_obj, "guild_id", None) or self._automate_sync: - _application_commands = self._loop.run_until_complete( - self._http.get_application_commands( - application_id=self.me.id, - guild_id=None - if not hasattr(_command_obj, "guild_id") - else _command_obj.guild_id, - ) - ) - _command_obj = self._find_command(_application_commands, command) - else: - for _scope in self._scopes: - _application_commands = self._loop.run_until_complete( - self._http.get_application_commands( - application_id=self.me.id, guild_id=_scope - ) - ) - _command_obj = self._find_command(_application_commands, command) - _command: Union[Snowflake, int] = int(_command_obj.id) - elif isinstance(command, int) or isinstance(command, Snowflake): - _command: Union[Snowflake, int] = int(command) - else: - raise ValueError( - "You can only insert strings, integers and ApplicationCommands here!" - ) # TODO: move to custom error formatter - - def decorator(coro: Coroutine) -> Any: - return self.event(coro, name=f"autocomplete_{_command}_{name}") - - return decorator - - def modal(self, modal: Union[Modal, str]) -> Callable[..., Any]: - """ - A decorator for listening to ``INTERACTION_CREATE`` dispatched gateway - events involving modals. - - .. error:: - This feature is currently under experimental/**beta access** - to those whitelisted for testing. Currently using this will - present you with an error with the modal not working. - - The structure for a modal callback: - - .. code-block:: python - - @modal(interactions.Modal( - interactions.TextInput( - style=interactions.TextStyleType.PARAGRAPH, - custom_id="how_was_your_day_field", - label="How has your day been?", - placeholder="Well, so far...", - ), - )) - async def modal_response(ctx): - ... - - The context of the modal callback decorator inherits the same - as of the component decorator. - - :param modal: The modal or custom_id of modal you wish to callback for. - :type modal: Union[Modal, str] - :return: A callable response. - :rtype: Callable[..., Any] - """ - - def decorator(coro: Coroutine) -> Any: - payload: str = modal.custom_id if isinstance(modal, Modal) else modal - return self.event(coro, name=f"modal_{payload}") - - return decorator - - def load( - self, name: str, package: Optional[str] = None, *args, **kwargs - ) -> Optional["Extension"]: - r""" - "Loads" an extension off of the current client by adding a new class - which is imported from the library. - - :param name: The name of the extension. - :type name: str - :param package?: The package of the extension. - :type package: Optional[str] - :param \*args?: Optional arguments to pass to the extension - :type \**args: tuple - :param \**kwargs?: Optional keyword-only arguments to pass to the extension. - :type \**kwargs: dict - :return: The loaded extension. - :rtype: Optional[Extension] - """ - _name: str = resolve_name(name, package) - - if _name in self._extensions: - log.error(f"Extension {name} has already been loaded. Skipping.") - return - - module = import_module( - name, package - ) # should be a module, because Extensions just need to be __init__-ed - - try: - setup = getattr(module, "setup") - extension = setup(self, *args, **kwargs) - except Exception as error: - del sys.modules[name] - log.error(f"Could not load {name}: {error}. Skipping.") - raise error from error - else: - log.debug(f"Loaded extension {name}.") - self._extensions[_name] = module - return extension - - def remove(self, name: str, package: Optional[str] = None) -> None: - """ - Removes an extension out of the current client from an import resolve. - - :param name: The name of the extension. - :type name: str - :param package?: The package of the extension. - :type package: Optional[str] - """ - try: - _name: str = resolve_name(name, package) - except AttributeError: - _name = name - - extension = self._extensions.get(_name) - - if _name not in self._extensions: - log.error(f"Extension {name} has not been loaded before. Skipping.") - return - - try: - extension.teardown() # made for Extension, usable by others - except AttributeError: - pass - - if isinstance(extension, ModuleType): # loaded as a module - for ext_name, ext in getmembers( - extension, lambda x: isinstance(x, type) and issubclass(x, Extension) - ): - self.remove(ext_name) - - del sys.modules[_name] - - del self._extensions[_name] - - log.debug(f"Removed extension {name}.") - - def reload( - self, name: str, package: Optional[str] = None, *args, **kwargs - ) -> Optional["Extension"]: - r""" - "Reloads" an extension off of current client from an import resolve. - - :param name: The name of the extension. - :type name: str - :param package?: The package of the extension. - :type package: Optional[str] - :param \*args?: Optional arguments to pass to the extension - :type \**args: tuple - :param \**kwargs?: Optional keyword-only arguments to pass to the extension. - :type \**kwargs: dict - :return: The reloaded extension. - :rtype: Optional[Extension] - """ - _name: str = resolve_name(name, package) - extension = self._extensions.get(_name) - - if extension is None: - log.warning(f"Extension {name} could not be reloaded because it was never loaded.") - self.load(name, package) - return - - self.remove(name, package) - return self.load(name, package, *args, **kwargs) - - def get_extension(self, name: str) -> Optional[Union[ModuleType, "Extension"]]: - return self._extensions.get(name) - - async def __raw_socket_create(self, data: Dict[Any, Any]) -> Dict[Any, Any]: - """ - This is an internal function that takes any gateway socket event - and then returns the data purely based off of what it does in - the client instantiation class. - - :param data: The data that is returned - :type data: Dict[Any, Any] - :return: A dictionary of raw data. - :rtype: Dict[Any, Any] - """ - - return data - - async def __raw_channel_create(self, channel) -> dict: - """ - This is an internal function that caches the channel creates when dispatched. - - :param channel: The channel object data in question. - :type channel: Channel - :return: The channel as a dictionary of raw data. - :rtype: dict - """ - self._http.cache.channels.add(Build(id=channel.id, value=channel)) - - return channel._json - - async def __raw_message_create(self, message) -> dict: - """ - This is an internal function that caches the message creates when dispatched. - - :param message: The message object data in question. - :type message: Message - :return: The message as a dictionary of raw data. - :rtype: dict - """ - self._http.cache.messages.add(Build(id=message.id, value=message)) - - return message._json - - async def __raw_guild_create(self, guild) -> dict: - """ - This is an internal function that caches the guild creates on ready. - - :param guild: The guild object data in question. - :type guild: Guild - :return: The guild as a dictionary of raw data. - :rtype: dict - """ - self._http.cache.self_guilds.add(Build(id=str(guild.id), value=guild)) - - return guild._json - - -# TODO: Implement the rest of cog behaviour when possible. -class Extension: - """ - A class that allows you to represent "extensions" of your code, or - essentially cogs that can be ran independent of the root file in - an object-oriented structure. - - The structure of an extension: - - .. code-block:: python - - class CoolCode(interactions.Extension): - def __init__(self, client): - self.client = client - - @command( - type=interactions.ApplicationCommandType.USER, - name="User command in cog", - ) - async def cog_user_cmd(self, ctx): - ... - - def setup(client): - CoolCode(client) - """ - - client: Client - - def __new__(cls, client: Client, *args, **kwargs) -> "Extension": - - self = super().__new__(cls) - - self.client = client - self._commands = {} - self._listeners = {} - - # This gets every coroutine in a way that we can easily change them - # cls - for name, func in getmembers(self, predicate=iscoroutinefunction): - - # TODO we can make these all share the same list, might make it easier to load/unload - if hasattr(func, "__listener_name__"): # set by extension_listener - func = client.event( - func, name=func.__listener_name__ - ) # capture the return value for friendlier ext-ing - - listeners = self._listeners.get(func.__listener_name__, []) - listeners.append(func) - self._listeners[func.__listener_name__] = listeners - - if hasattr(func, "__command_data__"): # Set by extension_command - args, kwargs = func.__command_data__ - func = client.command(*args, **kwargs)(func) - - cmd_name = f"command_{kwargs.get('name') or func.__name__}" - - commands = self._commands.get(cmd_name, []) - commands.append(func) - self._commands[cmd_name] = commands - - if hasattr(func, "__component_data__"): - args, kwargs = func.__component_data__ - func = client.component(*args, **kwargs)(func) - - component = kwargs.get("component") or args[0] - comp_name = ( - _component(component).custom_id - if isinstance(component, (Button, SelectMenu)) - else component - ) - comp_name = f"component_{comp_name}" - - listeners = self._listeners.get(comp_name, []) - listeners.append(func) - self._listeners[comp_name] = listeners - - if hasattr(func, "__autocomplete_data__"): - args, kwargs = func.__autocomplete_data__ - func = client.autocomplete(*args, **kwargs)(func) - - name = kwargs.get("name") or args[0] - _command = kwargs.get("command") or args[1] - - _command: Union[Snowflake, int] = ( - _command.id if isinstance(_command, ApplicationCommand) else _command - ) - - auto_name = f"autocomplete_{_command}_{name}" - - listeners = self._listeners.get(auto_name, []) - listeners.append(func) - self._listeners[auto_name] = listeners - - if hasattr(func, "__modal_data__"): - args, kwargs = func.__modal_data__ - func = client.modal(*args, **kwargs)(func) - - modal = kwargs.get("modal") or args[0] - _modal_id: str = modal.custom_id if isinstance(modal, Modal) else modal - modal_name = f"modal_{_modal_id}" - - listeners = self._listeners.get(modal_name, []) - listeners.append(func) - self._listeners[modal_name] = listeners - - client._extensions[cls.__name__] = self - - return self - - def teardown(self): - for event, funcs in self._listeners.items(): - for func in funcs: - self.client._websocket.dispatch.events[event].remove(func) - - for cmd, funcs in self._commands.items(): - for func in funcs: - self.client._websocket.dispatch.events[cmd].remove(func) - - clean_cmd_names = [cmd[7:] for cmd in self._commands.keys()] - cmds = filter( - lambda cmd_data: cmd_data["name"] in clean_cmd_names, - self.client._http.cache.interactions.view, - ) - - if self.client._automate_sync: - [ - self.client._loop.create_task( - self.client._http.delete_application_command( - cmd["application_id"], cmd["id"], cmd["guild_id"] - ) - ) - for cmd in cmds - ] - - -@wraps(command) -def extension_command(*args, **kwargs): - def decorator(coro): - coro.__command_data__ = (args, kwargs) - return coro - - return decorator - - -def extension_listener(name=None): - def decorator(func): - func.__listener_name__ = name or func.__name__ - - return func - - return decorator - - -@wraps(Client.component) -def extension_component(*args, **kwargs): - def decorator(func): - func.__component_data__ = (args, kwargs) - return func - - return decorator - - -@wraps(Client.autocomplete) -def extension_autocomplete(*args, **kwargs): - def decorator(func): - func.__autocomplete_data__ = (args, kwargs) - return func - - return decorator - - -@wraps(Client.modal) -def extension_modal(*args, **kwargs): - def decorator(func): - func.__modal_data__ = (args, kwargs) - return func - - return decorator - - -@wraps(Client.message_command) -def extension_message_command(*args, **kwargs): - def decorator(func): - kwargs["type"] = ApplicationCommandType.MESSAGE - func.__command_data__ = (args, kwargs) - return func - - return decorator - - -@wraps(Client.user_command) -def extension_user_command(*args, **kwargs): - def decorator(func): - kwargs["type"] = ApplicationCommandType.USER - func.__command_data__ = (args, kwargs) - return func - - return decorator From 6d4b5b83e61fb6a27bbc46beedde3061e394a8ba Mon Sep 17 00:00:00 2001 From: DeltaX <33706469+DeltaXWizard@users.noreply.github.com> Date: Fri, 6 May 2022 18:35:22 -0400 Subject: [PATCH 02/19] chore: Update requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e394e23b5..8e4cba24b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ aiohttp >= 3.8.1 -orjson From beec34cb484adb76247db12e3e477c34a4674902 Mon Sep 17 00:00:00 2001 From: DeltaXWizard <33706469+deltaxwizard@users.noreply.github.com> Date: Fri, 13 May 2022 11:44:58 -0400 Subject: [PATCH 03/19] chore: Version bump. --- interactions/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions/base.py b/interactions/base.py index a70c5c5eb..0632e28e3 100644 --- a/interactions/base.py +++ b/interactions/base.py @@ -1,6 +1,6 @@ import logging -__version__ = "4.2.0" +__version__ = "4.2.1" __authors__ = { "current": [ {"name": "DeltaX<@DeltaXWizard>", "status": "Project Maintainer"}, From 06203acd544a425d859959cfc7c566f3701cbef8 Mon Sep 17 00:00:00 2001 From: Sofia <41456914+ffl0w@users.noreply.github.com> Date: Wed, 18 May 2022 19:36:12 -0400 Subject: [PATCH 04/19] chore/ADMIN: force version onto old pypi This is an executive decision made between Delta and I. We will be forcing a version 4.0 push onto our old branch to help transition old users using v3 to our new project. This does not necessarily mean this branch will now be receiving updates along with the rest. The goal is to make our current branch used as much as possible. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35017c4cb..50e33b2ba 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read_requirements(filename): extras["dev"] = extras["lint"] + extras["readthedocs"] requirements = read_requirements("requirements.txt") setup( - name="discord-py-interactions", + name="discord-py-slash-command", version=VERSION, author="goverfl0w", author_email="james.discord.interactions@gmail.com", From 47122b5b12cbf6a07fcb94bd5a7982316115fdff Mon Sep 17 00:00:00 2001 From: James Walston <41456914+jameswalston@users.noreply.github.com> Date: Wed, 18 May 2022 19:45:20 -0400 Subject: [PATCH 05/19] chore/ADMIN: fix content type --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 50e33b2ba..9b9d58603 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def read_requirements(filename): install_requires=requirements, license="GPL-3.0 License", long_description=README, - long_description_content_type="text/x-rst", + # long_description_content_type="text/x-rst", url="https://github.com/interactions-py/library", packages=find_packages(), include_package_data=True, From 11b1c84f76d0ef7b1ac7f8bf770f50d0a20733af Mon Sep 17 00:00:00 2001 From: James Walston <41456914+jameswalston@users.noreply.github.com> Date: Wed, 18 May 2022 19:49:30 -0400 Subject: [PATCH 06/19] fix/ADMIN: correct improper description --- README.rst | 13 ------------- interactions.py.code-workspace | 4 +++- setup.py | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index e45eb46c9..09e5660a3 100644 --- a/README.rst +++ b/README.rst @@ -1,32 +1,19 @@ -=============== interactions.py =============== **Easy, simple, scalable and modular: a Python API wrapper for interactions.** .. image:: https://img.shields.io/pypi/dm/discord-py-slash-command.svg - :target: https://pypi.python.org/pypi/discord-py-interactions/ - :alt: PyPI Monthly Downloads .. image:: https://img.shields.io/github/license/goverfl0w/discord-interactions.svg - :target: https://github.com/goverfl0w/discord-interactions/blob/master/LICENSE - :alt: License .. image:: https://img.shields.io/pypi/pyversions/discord-py-interactions.svg - :target: https://pypi.python.org/pypi/discord-py-interactions/ - :alt: PyPI Versions .. image:: https://img.shields.io/pypi/v/discord-py-interactions.svg - :target: https://pypi.python.org/pypi/discord-py-interactions/ - :alt: PyPI Version .. image:: https://readthedocs.org/projects/discord-interactions/badge/?version=latest - :target: http://discord-interactions.readthedocs.io/?badge=latest - :alt: Documentation Status .. image:: https://discord.com/api/guilds/789032594456576001/embed.png - :target: https://discord.gg/KkgMBVuEkx - :alt: Discord ---- diff --git a/interactions.py.code-workspace b/interactions.py.code-workspace index 57097327f..7ba9b681e 100644 --- a/interactions.py.code-workspace +++ b/interactions.py.code-workspace @@ -4,5 +4,7 @@ "path": "." } ], - "settings": {} + "settings": { + "esbonio.sphinx.confDir": "" + } } diff --git a/setup.py b/setup.py index 9b9d58603..50e33b2ba 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def read_requirements(filename): install_requires=requirements, license="GPL-3.0 License", long_description=README, - # long_description_content_type="text/x-rst", + long_description_content_type="text/x-rst", url="https://github.com/interactions-py/library", packages=find_packages(), include_package_data=True, From 16ae28e263f26dda91b75d14f26b7fb60aba59fa Mon Sep 17 00:00:00 2001 From: James Walston <41456914+jameswalston@users.noreply.github.com> Date: Wed, 18 May 2022 19:50:44 -0400 Subject: [PATCH 07/19] revert/ADMIN: change back to original --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 50e33b2ba..35017c4cb 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ def read_requirements(filename): extras["dev"] = extras["lint"] + extras["readthedocs"] requirements = read_requirements("requirements.txt") setup( - name="discord-py-slash-command", + name="discord-py-interactions", version=VERSION, author="goverfl0w", author_email="james.discord.interactions@gmail.com", From dbd6e8504f30d6e01b2798efeeaa1186f202cbf5 Mon Sep 17 00:00:00 2001 From: Ventus <29584664+V3ntus@users.noreply.github.com> Date: Sat, 28 May 2022 23:20:03 -0500 Subject: [PATCH 08/19] ref: init new error handler --- interactions/api/error.py | 284 ++++++++++--------------------- interactions/api/error.pyi | 51 +----- interactions/api/http/request.py | 4 +- 3 files changed, 98 insertions(+), 241 deletions(-) diff --git a/interactions/api/error.py b/interactions/api/error.py index 3bef4ae26..5b7e46b15 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -1,162 +1,84 @@ -from enum import IntEnum -from string import Formatter -from typing import Any, Optional, Union +from typing import Optional, List +from logging import getLogger +log = getLogger(__name__) -class ErrorFormatter(Formatter): - """A customized error formatting script to return specific errors.""" - def get_value(self, key, args, kwargs) -> Any: - if not isinstance(key, str): - return Formatter.get_value(self, key=key, args=args, kwargs=kwargs) - try: - return kwargs[key] - except KeyError: - return key +class LibraryException(Exception): + code: Optional[int] + severity: int - -class InteractionException(Exception): - """ - An exception class for interactions. - - .. note:: - This is a WIP. This isn't meant to be used yet, this is a baseline, - and for extensive testing/review before integration. - Likewise, this will show the concepts before use, and will be refined when time goes on. - - :ivar interactions.api.error.ErrorFormatter _formatter: The built-in formatter. - :ivar dict _lookup: A dictionary containing the values from the built-in Enum. - - """ - - __slots__ = ("_type", "_lookup", "__type", "_formatter", "kwargs") - - def __init__(self, __type: Optional[Union[int, IntEnum]] = 0, **kwargs) -> None: - """ - :param __type: Type of error. This is decided from an IntEnum, which gives readable error messages instead of - typical error codes. Subclasses of this class works fine, and so does regular integers. - :type __type: Optional[Union[int, IntEnum]] - :param kwargs: Any additional keyword arguments. - :type **kwargs: dict - - :: note:: - (given if 3 is "DUPLICATE_COMMAND" and with the right enum import, it will display 3 as the error code.) - Example: - - >>> raise InteractionException(2, message="msg") - Exception raised: "msg" (error 2) - - >>> raise InteractionException(Default_Error_Enum.DUPLICATE_COMMAND) # noqa - Exception raised: Duplicate command name. (error 3) - """ - - self._type = __type - self.kwargs = kwargs - self._formatter = ErrorFormatter() - self._lookup = self.lookup() - - self.error() + __slots__ = {'code', 'severity', 'message', 'data'} @staticmethod - def lookup() -> dict: - """ - From the default error enum integer declaration, - generate a dictionary containing the phrases used for the errors. + def _parse(_data: dict) -> List[tuple]: """ - return { - 0: "Unknown error", - 1: "Request to Discord API has failed.", - 2: "Some formats are incorrect. See Discord API DOCS for proper format.", - 3: "There is a duplicate command name.", - 4: "There is a duplicate component callback.", - 5: "There are duplicate `Interaction` instances.", # rewrite to v4's interpretation - 6: "Command check has failed.", - 7: "Type passed was incorrect.", - 8: "Guild ID type passed was incorrect", - 9: "Incorrect data was passed to a slash command data object.", - 10: "The interaction was already responded to.", - 11: "Error creating your command.", - } + Internal function that should not be executed externally. + Parse the error data and set the code and message. - @property - def type(self) -> Optional[Union[int, IntEnum]]: + :param _data: The error data to parse. + :type _data: dict + :return: A list of tuples containing parsed errors. + :rtype: List[tuple] """ - Grabs the type attribute. - Primarily useful to use it in conditions for integral v4 (potential) logic. + _errors: list = [] + + def _inner(v, parent): + if isinstance(v, dict): + if (_ := v.get("_errors")) and isinstance(_, list): + for _ in _: + _errors.append((_["code"], _["message"], parent)) + else: + for k, v in v.items(): + if isinstance(v, dict): + _inner(v, parent + "." + k) + elif isinstance(v, list): + for i in v: + if isinstance(i, dict): + _errors.append((i["code"], i["message"], parent + "." + k)) + elif isinstance(v, list) and parent == "_errors": + for _ in v: + _errors.append((_["code"], _["message"], parent)) + + for _k, _v in _data.items(): + _inner(_v, _k) + return _errors + + def log(self, message: str, *args): """ - return self._type + Log the error message. - def error(self) -> None: - """This calls the exception.""" - _err_val = "" - _err_unidentifiable = False - _empty_space = " " - _overrided = "message" in self.kwargs - - if issubclass(type(self._type), IntEnum): - _err_val = self.type.name - _err_rep = self.type.value - elif type(self.type) == int: - _err_rep = self.type - else: # unidentifiable. - _err_rep = 0 - _err_unidentifiable = True - - _err_msg = _default_err_msg = "Error code: {_err_rep}" - - if self.kwargs != {} and _overrided: - _err_msg = self.kwargs["message"] - - self.kwargs["_err_rep"] = _err_rep - - if not _err_unidentifiable: - lookup_str = self._lookup[_err_rep] if _err_rep in self._lookup.keys() else _err_val - _lookup_str = ( - lookup_str - if max(self._lookup.keys()) >= _err_rep >= min(self._lookup.keys()) - else "" - ) - else: - _lookup_str = lookup_str = "" - - custom_err_str = ( - self._formatter.format(_err_msg, **self.kwargs) - if "_err_rep" in _err_msg - else self._formatter.format(_err_msg + _empty_space + _default_err_msg, **self.kwargs) - ) - - # This is just for writing notes meant to be for the developer(testers): - # - # Error code 4 represents dupe callback. In v3, that's "Duplicate component callback detected: " - # f"message ID {message_id or ''}, " - # f"custom_id `{custom_id or ''}`, " - # f"component_type `{component_type or ''}`" - # - # Error code 3 represents dupe command, i.e. "Duplicate command name detected: {name}" - # Error code 1 represents Req. failure, i.e. "Request failed with resp: {self.status} | {self.msg}" - # - - super().__init__( - f"{f'{lookup_str} ' if _err_val != '' else f'{_lookup_str + _empty_space if max(self._lookup.keys()) >= _err_rep >= min(self._lookup.keys()) else lookup_str}'}{custom_err_str}" - ) - - -class GatewayException(InteractionException): - """ - This is a derivation of InteractionException in that this is used to represent Gateway closing OP codes. - - :ivar ErrorFormatter _formatter: The built-in formatter. - :ivar dict _lookup: A dictionary containing the values from the built-in Enum. - """ - - __slots__ = ("_type", "_lookup", "__type", "_formatter", "kwargs") - - def __init__(self, __type, **kwargs): - super().__init__(__type, **kwargs) + :param message: + :type message: + :param args: + :type args: + """ + if self.severity == 0: # NOTSET + pass + elif self.severity == 10: # DEBUG + log.debug(message, *args) + elif self.severity == 20: # INFO + log.info(message, *args) + elif self.severity == 30: # WARNING + log.warning(message, *args) + elif self.severity == 40: # ERROR + log.error(message, *args) + elif self.severity == 50: # CRITICAL + log.critical(message, *args) @staticmethod - def lookup() -> dict: + def lookup(code: int) -> str: return { + 0: "Unknown error", + # HTTP errors + 400: "Bad Request. The request was improperly formatted, or the server couldn't understand it.", + 401: "Not authorized. Double check your token to see if it's valid.", + 403: "You do not have enough permissions to execute this.", + 404: "Resource does not exist.", + 405: "HTTP method not valid.", # ? + 429: "You are being rate limited. Please slow down on your requests.", # Definitely can be overclassed. + 502: "Gateway unavailable. Try again later.", + # Gateway errors 4000: "Unknown error. Try reconnecting?", 4001: "Unknown opcode. Check your gateway opcode and/or payload.", 4002: "Invalid payload.", @@ -171,52 +93,7 @@ def lookup() -> dict: 4012: "Invalid API version for the Gateway.", 4013: "Invalid intent(s).", 4014: "Some intent(s) requested are not allowed. Please double check.", - } - - -class HTTPException(InteractionException): - """ - This is a derivation of InteractionException in that this is used to represent HTTP Exceptions. - - :ivar ErrorFormatter _formatter: The built-in formatter. - :ivar dict _lookup: A dictionary containing the values from the built-in Enum. - """ - - __slots__ = ("_type", "_lookup", "__type", "_formatter", "kwargs") - - def __init__(self, __type, **kwargs): - super().__init__(__type, **kwargs) - - @staticmethod - def lookup() -> dict: - return { - 400: "Bad Request. The request was improperly formatted, or the server couldn't understand it.", - 401: "Not authorized. Double check your token to see if it's valid.", - 403: "You do not have enough permissions to execute this.", - 404: "Resource does not exist.", - 405: "HTTP method not valid.", # ? - 429: "You are being rate limited. Please slow down on your requests.", # Definitely can be overclassed. - 502: "Gateway unavailable. Try again later.", - } - - -class JSONException(InteractionException): - """ - This is a derivation of InteractionException in that this is used to represent JSON API Exceptions. - - :ivar ErrorFormatter _formatter: The built-in formatter. - :ivar dict _lookup: A dictionary containing the values from the built-in Enum. - """ - - __slots__ = ("_type", "_lookup", "__type", "_formatter", "kwargs") - - def __init__(self, __type, **kwargs): - super().__init__(__type, **kwargs) - - @staticmethod - def lookup() -> dict: - return { - 0: "Unknown Error.", + # JSON errors 10001: "Unknown Account.", 10002: "Unknown Application.", 10003: "Unknown Channel.", @@ -266,7 +143,7 @@ def lookup() -> dict: 20022: "This message cannot be edited due to announcement rate limits.", 20028: "The channel you are writing has hit the write rate limit", 20031: "Your Stage topic, server name, server description, " - "or channel names contain words that are not allowed", + "or channel names contain words that are not allowed", 20035: "Guild premium subscription level too low", 30001: "Maximum number of guilds reached (100)", 30002: "Maximum number of friends reached (1000)", @@ -318,7 +195,7 @@ def lookup() -> dict: 50014: "Invalid authentication token provided", 50015: "Note was too long", 50016: "Provided too few or too many messages to delete. " - "Must provide at least 2 and fewer than 100 messages to delete", + "Must provide at least 2 and fewer than 100 messages to delete", 50019: "A message can only be pinned to the channel it was sent in", 50020: "Invite code was either invalid or taken", 50021: "Cannot execute action on a system message", @@ -330,7 +207,7 @@ def lookup() -> dict: 50033: "Invalid Recipient(s)", 50034: "A message provided was too old to bulk delete", 50035: "Invalid form body (returned for both application/json and multipart/form-data bodies)," - " or invalid Content-Type provided", + " or invalid Content-Type provided", 50036: "An invite was accepted to a guild the application's bot is not in", 50041: "Invalid API version provided", 50045: "File uploaded exceeds the maximum size", @@ -342,7 +219,7 @@ def lookup() -> dict: 50074: "Cannot delete a channel required for Community guilds", 50081: "Invalid sticker sent", 50083: "Tried to perform an operation on an archived thread, such as editing a " - "message or adding a user to the thread", + "message or adding a user to the thread", 50084: "Invalid thread notification settings", 50085: "'before' value is earlier than the thread creation date", 50086: "Community server channels must be text channels", @@ -369,4 +246,19 @@ def lookup() -> dict: 170007: "Sticker animation duration exceeds maximum of 5 seconds", 180000: "Cannot update a finished event", 180002: "Failed to create stage needed for stage event", - } + }.get(code, f"Unknown error: {code}") + + def __init__(self, message: str = None, code: int = 0, severity: int = 0, **kwargs): + self.code: int = code + self.severity: int = severity + self.data: dict = kwargs.pop('data', None) + self.message: str = message or self.lookup(self.code) + _fmt_error: List[tuple] = [] + + if self.data and isinstance(self.data, dict) and \ + isinstance(self.data.get('errors', None), dict): + _fmt_error: List[tuple] = [i for i in self._parse(self.data['errors'])] + + super().__init__(f"{self.message} (code: {self.code}, severity {self.severity})\n" + "\n".join( + [f"Error at {i[2]}: {i[0]} - {i[1]}" for i in _fmt_error] + ) if _fmt_error else None) diff --git a/interactions/api/error.pyi b/interactions/api/error.pyi index af4862dcc..2ebf478b4 100644 --- a/interactions/api/error.pyi +++ b/interactions/api/error.pyi @@ -1,49 +1,14 @@ -from enum import IntEnum -from string import Formatter -from typing import Any, Dict, Optional, Union +from typing import Optional, List -class ErrorFormatter(Formatter): - def get_value(self, key, args, kwargs) -> Any: ... +class LibraryException(Exception): + message: Optional[str] + code: Optional[int] + severity: Optional[int] -class InteractionException(Exception): - _type: Union[int, IntEnum] - __type: Optional[Union[int, IntEnum]] - _formatter: ErrorFormatter - kwargs: Dict[str, Any] - _lookup: dict - def __init__(self, __type: Optional[Union[int, IntEnum]] = 0, **kwargs) -> None: ... @staticmethod - def lookup() -> dict: ... - @property - def type(self) -> Optional[Union[int, IntEnum]]: ... - def error(self) -> None: ... + def _parse(_data: dict) -> List[tuple]: ... -class GatewayException(InteractionException): - _type: Union[int, IntEnum] - __type: Optional[Union[int, IntEnum]] - _formatter: ErrorFormatter - kwargs: Dict[str, Any] - _lookup: dict - def __init__(self, __type, **kwargs): ... - @staticmethod - def lookup() -> dict: ... - -class HTTPException(InteractionException): - _type: Union[int, IntEnum] - __type: Optional[Union[int, IntEnum]] - _formatter: ErrorFormatter - kwargs: Dict[str, Any] - _lookup: dict - def __init__(self, __type, **kwargs): ... - @staticmethod - def lookup() -> dict: ... + def log(self, message: str, *args) -> None: ... -class JSONException(InteractionException): - _type: Union[int, IntEnum] - __type: Optional[Union[int, IntEnum]] - _formatter: ErrorFormatter - kwargs: Dict[str, Any] - _lookup: dict - def __init__(self, __type, **kwargs): ... @staticmethod - def lookup() -> dict: ... + def lookup(code: int) -> str: ... diff --git a/interactions/api/http/request.py b/interactions/api/http/request.py index a86e5ebc7..0e60a3140 100644 --- a/interactions/api/http/request.py +++ b/interactions/api/http/request.py @@ -12,7 +12,7 @@ from interactions.base import __version__, get_logger -from ...api.error import HTTPException +from ...api.error import LibraryException from .limiter import Limiter from .route import Route @@ -168,7 +168,7 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]: ) # This "redundant" debug line is for debug use and tracing back the error codes. - raise HTTPException(data["code"], message=data["message"]) + raise LibraryException(message=data["message"], code=data["code"]) if response.status == 429: if not is_global: From 3e93dd3e068c664742947cdec35a4bb2f262a66c Mon Sep 17 00:00:00 2001 From: Ventus <29584664+V3ntus@users.noreply.github.com> Date: Sat, 11 Jun 2022 15:33:38 -0500 Subject: [PATCH 09/19] add: default error enums (EdVraz) --- interactions/api/error.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/interactions/api/error.py b/interactions/api/error.py index 5b7e46b15..c52c62d43 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -69,7 +69,19 @@ def log(self, message: str, *args): @staticmethod def lookup(code: int) -> str: return { + # Default error integer enum 0: "Unknown error", + 1: "Request to Discord API has failed.", + 2: "Some formats are incorrect. See Discord API DOCS for proper format.", + 3: "There is a duplicate command name.", + 4: "There is a duplicate component callback.", + 5: "There are duplicate `Interaction` instances.", # rewrite to v4's interpretation + 6: "Command check has failed.", + 7: "Type passed was incorrect.", + 8: "Guild ID type passed was incorrect", + 9: "Incorrect data was passed to a slash command data object.", + 10: "The interaction was already responded to.", + 11: "Error creating your command.", # HTTP errors 400: "Bad Request. The request was improperly formatted, or the server couldn't understand it.", 401: "Not authorized. Double check your token to see if it's valid.", From 3c2ef419cf209e006ade4ebd721f4fcf359722e6 Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Sun, 12 Jun 2022 20:10:37 +0200 Subject: [PATCH 10/19] refactor: Change exception raising when requests are made --- interactions/api/http/request.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/interactions/api/http/request.py b/interactions/api/http/request.py index 18cdb3a12..ecadeeccc 100644 --- a/interactions/api/http/request.py +++ b/interactions/api/http/request.py @@ -12,7 +12,7 @@ from interactions.base import __version__, get_logger -from ...api.error import HTTPException +from ...api.error import HTTPException, JSONException from .limiter import Limiter from .route import Route @@ -163,13 +163,16 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]: self.buckets[route.endpoint] = _bucket # real-time replacement/update/add if needed. - if isinstance(data, dict) and data.get("errors"): + if isinstance(data, dict) and (data.get("errors") or (data.get("code") and data.get("code") != 429)): log.debug( f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}" ) # This "redundant" debug line is for debug use and tracing back the error codes. - raise HTTPException(data["code"], message=data["message"]) + if int(data["code"]) in JSONException.lookup().keys(): + raise JSONException(data["code"], message=JSONException.lookup()[int(data["code"])]) + else: + raise HTTPException(data["code"], message=data["message"]) if response.status == 429: if not is_global: From 11c2c756db0bd5f1c0913338e92624efe23738c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 12 Jun 2022 18:14:04 +0000 Subject: [PATCH 11/19] ci: correct from checks. --- interactions/api/http/request.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/interactions/api/http/request.py b/interactions/api/http/request.py index ecadeeccc..5cdc90049 100644 --- a/interactions/api/http/request.py +++ b/interactions/api/http/request.py @@ -163,14 +163,18 @@ async def request(self, route: Route, **kwargs) -> Optional[Any]: self.buckets[route.endpoint] = _bucket # real-time replacement/update/add if needed. - if isinstance(data, dict) and (data.get("errors") or (data.get("code") and data.get("code") != 429)): + if isinstance(data, dict) and ( + data.get("errors") or (data.get("code") and data.get("code") != 429) + ): log.debug( f"RETURN {response.status}: {dumps(data, indent=4, sort_keys=True)}" ) # This "redundant" debug line is for debug use and tracing back the error codes. if int(data["code"]) in JSONException.lookup().keys(): - raise JSONException(data["code"], message=JSONException.lookup()[int(data["code"])]) + raise JSONException( + data["code"], message=JSONException.lookup()[int(data["code"])] + ) else: raise HTTPException(data["code"], message=data["message"]) From 64c7a610830372ebb6764b364b2929317b200d4a Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Sun, 12 Jun 2022 20:28:52 +0200 Subject: [PATCH 12/19] refactor: Change all imports and `raise`s to `LibraryException` --- interactions/api/gateway/client.py | 4 +- interactions/api/models/message.py | 4 +- interactions/client/bot.py | 72 ++++++++++++------------- interactions/client/models/component.py | 4 +- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/interactions/api/gateway/client.py b/interactions/api/gateway/client.py index 075ece295..529966c02 100644 --- a/interactions/api/gateway/client.py +++ b/interactions/api/gateway/client.py @@ -24,7 +24,7 @@ from ...client.models import Option from ..dispatch import Listener from ..enums import OpCodeType -from ..error import GatewayException +from ..error import LibraryException from ..http.client import HTTPClient from ..models.attrs_utils import MISSING from ..models.flags import Intents @@ -186,7 +186,7 @@ async def _establish_connection( break if self._client.close_code in range(4010, 4014) or self._client.close_code == 4004: - raise GatewayException(self._client.close_code) + raise LibraryException(self._client.close_code) await self._handle_connection(stream, shard, presence) diff --git a/interactions/api/models/message.py b/interactions/api/models/message.py index 3a3e279ac..72f0ae035 100644 --- a/interactions/api/models/message.py +++ b/interactions/api/models/message.py @@ -4,7 +4,7 @@ from attrs import converters -from ..error import JSONException +from ..error import LibraryException from .attrs_utils import ( MISSING, ClientSerializerMixin, @@ -972,7 +972,7 @@ async def edit( ) if code := _dct.get("code"): - raise JSONException(code, message=_dct.get("message")) + raise LibraryException(code, message=_dct.get("message")) self.update(_dct) diff --git a/interactions/client/bot.py b/interactions/client/bot.py index 672604c0e..d15802080 100644 --- a/interactions/client/bot.py +++ b/interactions/client/bot.py @@ -12,7 +12,7 @@ from ..api import Cache from ..api import Item as Build from ..api import WebSocketClient as WSClient -from ..api.error import InteractionException, JSONException +from ..api.error import LibraryException from ..api.http.client import HTTPClient from ..api.models.attrs_utils import MISSING from ..api.models.flags import Intents, Permissions @@ -403,7 +403,7 @@ async def __get_all_commands(self) -> None: for command in _cmds: if command.get("code"): # Error exists. - raise JSONException(command["code"], message=f'{command["message"]} |') + raise LibraryException(command["code"], message=f'{command["message"]} |') self.__global_commands = {"commands": _cmds, "clean": True} # TODO: add to cache (later) @@ -417,7 +417,7 @@ async def __get_all_commands(self) -> None: if isinstance(_cmds, dict) and _cmds.get("code"): if int(_cmds.get("code")) != 50001: - raise JSONException(_cmds["code"], message=f'{_cmds["message"]} |') + raise LibraryException(_cmds["code"], message=f'{_cmds["message"]} |') log.warning( f"Your bot is missing access to guild with corresponding id {_id}! " @@ -429,7 +429,7 @@ async def __get_all_commands(self) -> None: for command in _cmds: if command.get("code"): # Error exists. - raise JSONException(command["code"], message=f'{command["message"]} |') + raise LibraryException(command["code"], message=f'{command["message"]} |') self.__guild_commands[_id] = {"commands": _cmds, "clean": True} @@ -452,7 +452,7 @@ async def __sync(self) -> None: # sourcery no-metrics for command in _cmds: if command.get("code"): # Error exists. - raise JSONException(command["code"], message=f'{command["message"]} |') + raise LibraryException(command["code"], message=f'{command["message"]} |') self.__global_commands = {"commands": _cmds, "clean": True} # TODO: add to cache (later) @@ -471,7 +471,7 @@ async def __sync(self) -> None: # sourcery no-metrics if isinstance(_cmds, dict) and _cmds.get("code"): # Error exists. if int(_cmds.get("code")) != 50001: - raise JSONException(_cmds["code"], message=f'{_cmds["message"]} |') + raise LibraryException(_cmds["code"], message=f'{_cmds["message"]} |') log.warning( f"Your bot is missing access to guild with corresponding id {_id}! " @@ -485,7 +485,7 @@ async def __sync(self) -> None: # sourcery no-metrics for command in _cmds: if command.get("code"): # Error exists. - raise JSONException(command["code"], message=f'{command["message"]} |') + raise LibraryException(command["code"], message=f'{command["message"]} |') self.__guild_commands[_id] = {"commands": _cmds, "clean": True} __check_guild_commands[_id] = [cmd["name"] for cmd in _cmds] if _cmds else [] @@ -498,7 +498,7 @@ async def __sync(self) -> None: # sourcery no-metrics _guild_id = _guild_command.get("guild_id") if _guild_id in __blocked_guilds: log.fatal(f"Cannot sync commands on guild with id {_guild_id}!") - raise JSONException(50001, message="Missing Access |") + raise LibraryException(50001, message="Missing Access |") if _guild_command["name"] not in __check_guild_commands[_guild_id]: self.__guild_commands[_guild_id]["clean"] = False self.__guild_commands[_guild_id]["commands"].append(_guild_command) @@ -644,27 +644,27 @@ def __check_sub_group(_sub_group: Option): nonlocal _sub_groups_present _sub_groups_present = True if _sub_group.name is MISSING: - raise InteractionException(11, message="Sub command groups must have a name.") + raise LibraryException(11, message="Sub command groups must have a name.") __indent = 4 log.debug( f"{' ' * __indent}checking sub command group '{_sub_group.name}' of command '{command.name}'" ) if not re.fullmatch(reg, _sub_group.name): - raise InteractionException( + raise LibraryException( 11, message=f"The sub command group name does not match the regex for valid names ('{regex}')", ) elif _sub_group.description is MISSING and not _sub_group.description: - raise InteractionException(11, message="A description is required.") + raise LibraryException(11, message="A description is required.") elif len(_sub_group.description) > 100: - raise InteractionException( + raise LibraryException( 11, message="Descriptions must be less than 100 characters." ) if not _sub_group.options: - raise InteractionException(11, message="sub command groups must have subcommands!") + raise LibraryException(11, message="sub command groups must have subcommands!") if len(_sub_group.options) > 25: - raise InteractionException( + raise LibraryException( 11, message="A sub command group cannot contain more than 25 sub commands!" ) for _sub_command in _sub_group.options: @@ -674,7 +674,7 @@ def __check_sub_command(_sub_command: Option, _sub_group: Option = MISSING): nonlocal _sub_cmds_present _sub_cmds_present = True if _sub_command.name is MISSING: - raise InteractionException(11, message="sub commands must have a name!") + raise LibraryException(11, message="sub commands must have a name!") if _sub_group is not MISSING: __indent = 8 log.debug( @@ -686,20 +686,20 @@ def __check_sub_command(_sub_command: Option, _sub_group: Option = MISSING): f"{' ' * __indent}checking sub command '{_sub_command.name}' of command '{command.name}'" ) if not re.fullmatch(reg, _sub_command.name): - raise InteractionException( + raise LibraryException( 11, message=f"The sub command name does not match the regex for valid names ('{reg}')", ) elif _sub_command.description is MISSING or not _sub_command.description: - raise InteractionException(11, message="A description is required.") + raise LibraryException(11, message="A description is required.") elif len(_sub_command.description) > 100: - raise InteractionException( + raise LibraryException( 11, message="Descriptions must be less than 100 characters." ) if _sub_command.options is not MISSING and _sub_command.options: if len(_sub_command.options) > 25: - raise InteractionException( + raise LibraryException( 11, message="Your sub command must have less than 25 options." ) _sub_opt_names = [] @@ -712,7 +712,7 @@ def __check_options(_option: Option, _names: list, _sub_command: Option = MISSIN if getattr(_option, "autocomplete", False) and getattr(_option, "choices", False): log.warning("Autocomplete may not be set to true if choices are present.") if _option.name is MISSING: - raise InteractionException(11, message="Options must have a name.") + raise LibraryException(11, message="Options must have a name.") if _sub_command is not MISSING: __indent = 12 if _sub_groups_present else 8 log.debug( @@ -725,22 +725,22 @@ def __check_options(_option: Option, _names: list, _sub_command: Option = MISSIN ) _options_names.append(_option.name) if not re.fullmatch(reg, _option.name): - raise InteractionException( + raise LibraryException( 11, message=f"The option name does not match the regex for valid names ('{regex}')", ) if _option.description is MISSING or not _option.description: - raise InteractionException( + raise LibraryException( 11, message="A description is required.", ) elif len(_option.description) > 100: - raise InteractionException( + raise LibraryException( 11, message="Descriptions must be less than 100 characters.", ) if _option.name in _names: - raise InteractionException( + raise LibraryException( 11, message="You must not have two options with the same name in a command!" ) _names.append(_option.name) @@ -752,17 +752,17 @@ def __check_coro(): if not len(coro.__code__.co_varnames) ^ ( _ismethod and len(coro.__code__.co_varnames) == 1 ): - raise InteractionException( + raise LibraryException( 11, message="Your command needs at least one argument to return context." ) elif "kwargs" in coro.__code__.co_varnames: return elif _sub_cmds_present and len(coro.__code__.co_varnames) < (3 if _ismethod else 2): - raise InteractionException( + raise LibraryException( 11, message="Your command needs one argument for the sub_command." ) elif _sub_groups_present and len(coro.__code__.co_varnames) < (4 if _ismethod else 3): - raise InteractionException( + raise LibraryException( 11, message="Your command needs one argument for the sub_command and one for the sub_command_group.", ) @@ -775,12 +775,12 @@ def __check_coro(): "Coroutine is missing arguments for options:" f" {[_arg for _arg in _options_names if _arg not in coro.__code__.co_varnames]}" ) - raise InteractionException( + raise LibraryException( 11, message="You need one argument for every option name in your command!" ) if command.name is MISSING: - raise InteractionException(11, message="Your command must have a name.") + raise LibraryException(11, message="Your command must have a name.") else: log.debug(f"checking command '{command.name}':") @@ -788,31 +788,31 @@ def __check_coro(): not re.fullmatch(reg, command.name) and command.type == ApplicationCommandType.CHAT_INPUT ): - raise InteractionException( + raise LibraryException( 11, message=f"Your command does not match the regex for valid names ('{regex}')" ) elif command.type == ApplicationCommandType.CHAT_INPUT and ( command.description is MISSING or not command.description ): - raise InteractionException(11, message="A description is required.") + raise LibraryException(11, message="A description is required.") elif command.type != ApplicationCommandType.CHAT_INPUT and ( command.description is not MISSING and command.description ): - raise InteractionException( + raise LibraryException( 11, message="Only chat-input commands can have a description." ) elif command.description is not MISSING and len(command.description) > 100: - raise InteractionException(11, message="Descriptions must be less than 100 characters.") + raise LibraryException(11, message="Descriptions must be less than 100 characters.") if command.options and command.options is not MISSING: if len(command.options) > 25: - raise InteractionException( + raise LibraryException( 11, message="Your command must have less than 25 options." ) if command.type != ApplicationCommandType.CHAT_INPUT: - raise InteractionException( + raise LibraryException( 11, message="Only CHAT_INPUT commands can have options/sub-commands!" ) @@ -1167,7 +1167,7 @@ def _find_command(self, command: str) -> ApplicationCommand: break if not _command_obj or (hasattr(_command_obj, "id") and not _command_obj.id): - raise InteractionException( + raise LibraryException( 6, message="The command does not exist. Make sure to define" + " your autocomplete callback after your commands", diff --git a/interactions/client/models/component.py b/interactions/client/models/component.py index e0e04a3ff..9e7553ad8 100644 --- a/interactions/client/models/component.py +++ b/interactions/client/models/component.py @@ -1,6 +1,6 @@ from typing import List, Optional -from ...api.error import InteractionException +from ...api.error import LibraryException from ...api.models.attrs_utils import MISSING, DictSerializerMixin, convert_list, define, field from ...api.models.message import Emoji from ..enums import ButtonStyle, ComponentType, TextStyleType @@ -395,7 +395,7 @@ def __check_components(): ) return _components else: - raise InteractionException( + raise LibraryException( 11, message="The specified components are invalid and could not be created!" ) From 129a9c880ca6ea6eab21c05853e1764db83f17d6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 12 Jun 2022 18:37:12 +0000 Subject: [PATCH 13/19] ci: correct from checks. --- interactions/api/error.py | 36 ++++++++++++++++++++---------------- interactions/client/bot.py | 16 ++++------------ 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/interactions/api/error.py b/interactions/api/error.py index f5f016d4a..d33c8e567 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -1,9 +1,7 @@ -from typing import Optional, List from logging import getLogger +from typing import List, Optional -__all__ = ( - "LibraryException", -) +__all__ = ("LibraryException",) log = getLogger(__name__) @@ -12,7 +10,7 @@ class LibraryException(Exception): code: Optional[int] severity: int - __slots__ = {'code', 'severity', 'message', 'data'} + __slots__ = {"code", "severity", "message", "data"} @staticmethod def _parse(_data: dict) -> List[tuple]: @@ -159,7 +157,7 @@ def lookup(code: int) -> str: 20022: "This message cannot be edited due to announcement rate limits.", 20028: "The channel you are writing has hit the write rate limit", 20031: "Your Stage topic, server name, server description, " - "or channel names contain words that are not allowed", + "or channel names contain words that are not allowed", 20035: "Guild premium subscription level too low", 30001: "Maximum number of guilds reached (100)", 30002: "Maximum number of friends reached (1000)", @@ -211,7 +209,7 @@ def lookup(code: int) -> str: 50014: "Invalid authentication token provided", 50015: "Note was too long", 50016: "Provided too few or too many messages to delete. " - "Must provide at least 2 and fewer than 100 messages to delete", + "Must provide at least 2 and fewer than 100 messages to delete", 50019: "A message can only be pinned to the channel it was sent in", 50020: "Invite code was either invalid or taken", 50021: "Cannot execute action on a system message", @@ -223,7 +221,7 @@ def lookup(code: int) -> str: 50033: "Invalid Recipient(s)", 50034: "A message provided was too old to bulk delete", 50035: "Invalid form body (returned for both application/json and multipart/form-data bodies)," - " or invalid Content-Type provided", + " or invalid Content-Type provided", 50036: "An invite was accepted to a guild the application's bot is not in", 50041: "Invalid API version provided", 50045: "File uploaded exceeds the maximum size", @@ -235,7 +233,7 @@ def lookup(code: int) -> str: 50074: "Cannot delete a channel required for Community guilds", 50081: "Invalid sticker sent", 50083: "Tried to perform an operation on an archived thread, such as editing a " - "message or adding a user to the thread", + "message or adding a user to the thread", 50084: "Invalid thread notification settings", 50085: "'before' value is earlier than the thread creation date", 50086: "Community server channels must be text channels", @@ -267,14 +265,20 @@ def lookup(code: int) -> str: def __init__(self, message: str = None, code: int = 0, severity: int = 0, **kwargs): self.code: int = code self.severity: int = severity - self.data: dict = kwargs.pop('data', None) + self.data: dict = kwargs.pop("data", None) self.message: str = message or self.lookup(self.code) _fmt_error: List[tuple] = [] - if self.data and isinstance(self.data, dict) and \ - isinstance(self.data.get('errors', None), dict): - _fmt_error: List[tuple] = [i for i in self._parse(self.data['errors'])] + if ( + self.data + and isinstance(self.data, dict) + and isinstance(self.data.get("errors", None), dict) + ): + _fmt_error: List[tuple] = [i for i in self._parse(self.data["errors"])] - super().__init__(f"{self.message} (code: {self.code}, severity {self.severity})\n" + "\n".join( - [f"Error at {i[2]}: {i[0]} - {i[1]}" for i in _fmt_error] - ) if _fmt_error else None) + super().__init__( + f"{self.message} (code: {self.code}, severity {self.severity})\n" + + "\n".join([f"Error at {i[2]}: {i[0]} - {i[1]}" for i in _fmt_error]) + if _fmt_error + else None + ) diff --git a/interactions/client/bot.py b/interactions/client/bot.py index d15802080..1a5951e1d 100644 --- a/interactions/client/bot.py +++ b/interactions/client/bot.py @@ -657,9 +657,7 @@ def __check_sub_group(_sub_group: Option): elif _sub_group.description is MISSING and not _sub_group.description: raise LibraryException(11, message="A description is required.") elif len(_sub_group.description) > 100: - raise LibraryException( - 11, message="Descriptions must be less than 100 characters." - ) + raise LibraryException(11, message="Descriptions must be less than 100 characters.") if not _sub_group.options: raise LibraryException(11, message="sub command groups must have subcommands!") @@ -693,9 +691,7 @@ def __check_sub_command(_sub_command: Option, _sub_group: Option = MISSING): elif _sub_command.description is MISSING or not _sub_command.description: raise LibraryException(11, message="A description is required.") elif len(_sub_command.description) > 100: - raise LibraryException( - 11, message="Descriptions must be less than 100 characters." - ) + raise LibraryException(11, message="Descriptions must be less than 100 characters.") if _sub_command.options is not MISSING and _sub_command.options: if len(_sub_command.options) > 25: @@ -798,18 +794,14 @@ def __check_coro(): elif command.type != ApplicationCommandType.CHAT_INPUT and ( command.description is not MISSING and command.description ): - raise LibraryException( - 11, message="Only chat-input commands can have a description." - ) + raise LibraryException(11, message="Only chat-input commands can have a description.") elif command.description is not MISSING and len(command.description) > 100: raise LibraryException(11, message="Descriptions must be less than 100 characters.") if command.options and command.options is not MISSING: if len(command.options) > 25: - raise LibraryException( - 11, message="Your command must have less than 25 options." - ) + raise LibraryException(11, message="Your command must have less than 25 options.") if command.type != ApplicationCommandType.CHAT_INPUT: raise LibraryException( From 1d70f306eb42d7fdd2b63a03eb7f268bb19fcab4 Mon Sep 17 00:00:00 2001 From: Ventus <29584664+V3ntus@users.noreply.github.com> Date: Sun, 12 Jun 2022 13:51:25 -0500 Subject: [PATCH 14/19] ref: variable renaming for readability Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> --- interactions/api/error.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interactions/api/error.py b/interactions/api/error.py index d33c8e567..cf52af721 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -27,9 +27,9 @@ def _parse(_data: dict) -> List[tuple]: def _inner(v, parent): if isinstance(v, dict): - if (_ := v.get("_errors")) and isinstance(_, list): - for _ in _: - _errors.append((_["code"], _["message"], parent)) + if (errs := v.get("_errors")) and isinstance(errs, list): + for err in errs: + _errors.append((err["code"], err["message"], parent)) else: for k, v in v.items(): if isinstance(v, dict): From c2bd76c00949ff7c91eae26da9369b390db8ca05 Mon Sep 17 00:00:00 2001 From: Ventus <29584664+V3ntus@users.noreply.github.com> Date: Sun, 12 Jun 2022 13:51:37 -0500 Subject: [PATCH 15/19] ref: variable renaming for readability Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> --- interactions/api/error.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interactions/api/error.py b/interactions/api/error.py index cf52af721..668d8039c 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -35,9 +35,9 @@ def _inner(v, parent): if isinstance(v, dict): _inner(v, parent + "." + k) elif isinstance(v, list): - for i in v: - if isinstance(i, dict): - _errors.append((i["code"], i["message"], parent + "." + k)) + for e in v: + if isinstance(e, dict): + _errors.append((e["code"], e["message"], parent + "." + k)) elif isinstance(v, list) and parent == "_errors": for _ in v: _errors.append((_["code"], _["message"], parent)) From 12c5e292c8c0fc104b85447b7b5e1ae5a4d56672 Mon Sep 17 00:00:00 2001 From: Ventus <29584664+V3ntus@users.noreply.github.com> Date: Sun, 12 Jun 2022 13:51:47 -0500 Subject: [PATCH 16/19] ref: variable renaming for readability Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> --- interactions/api/error.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interactions/api/error.py b/interactions/api/error.py index 668d8039c..7918e2684 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -39,8 +39,8 @@ def _inner(v, parent): if isinstance(e, dict): _errors.append((e["code"], e["message"], parent + "." + k)) elif isinstance(v, list) and parent == "_errors": - for _ in v: - _errors.append((_["code"], _["message"], parent)) + for e in v: + _errors.append((e["code"], e["message"], parent)) for _k, _v in _data.items(): _inner(_v, _k) From eb6ca8ea068fcb4aa502e446aeec47c549cd923a Mon Sep 17 00:00:00 2001 From: Ventus <29584664+V3ntus@users.noreply.github.com> Date: Sun, 12 Jun 2022 13:52:05 -0500 Subject: [PATCH 17/19] ref: variable renaming for readability Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> --- interactions/api/error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions/api/error.py b/interactions/api/error.py index 7918e2684..a3d9ba375 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -278,7 +278,7 @@ def __init__(self, message: str = None, code: int = 0, severity: int = 0, **kwar super().__init__( f"{self.message} (code: {self.code}, severity {self.severity})\n" - + "\n".join([f"Error at {i[2]}: {i[0]} - {i[1]}" for i in _fmt_error]) + + "\n".join([f"Error at {e[2]}: {e[0]} - {e[1]}" for e in _fmt_error]) if _fmt_error else None ) From efce0b47abef92aa90632b7c3489957d96085cf1 Mon Sep 17 00:00:00 2001 From: Ventus <29584664+V3ntus@users.noreply.github.com> Date: Sun, 12 Jun 2022 13:55:58 -0500 Subject: [PATCH 18/19] ref: simplify a dumb list comp thing I made Co-authored-by: Toricane <73972068+Toricane@users.noreply.github.com> --- interactions/api/error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactions/api/error.py b/interactions/api/error.py index a3d9ba375..e5acc9014 100644 --- a/interactions/api/error.py +++ b/interactions/api/error.py @@ -274,7 +274,7 @@ def __init__(self, message: str = None, code: int = 0, severity: int = 0, **kwar and isinstance(self.data, dict) and isinstance(self.data.get("errors", None), dict) ): - _fmt_error: List[tuple] = [i for i in self._parse(self.data["errors"])] + _fmt_error: List[tuple] = self._parse(self.data["errors"]) super().__init__( f"{self.message} (code: {self.code}, severity {self.severity})\n" From 634b4d96881da1d458fd5d787855dcffde8d412f Mon Sep 17 00:00:00 2001 From: EdVraz <88881326+EdVraz@users.noreply.github.com> Date: Sun, 12 Jun 2022 21:05:48 +0200 Subject: [PATCH 19/19] refactor: Change all imports and `raise`s to `LibraryException` --- interactions/client/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interactions/client/context.py b/interactions/client/context.py index 3026f2079..c183eec78 100644 --- a/interactions/client/context.py +++ b/interactions/client/context.py @@ -1,7 +1,7 @@ from logging import Logger from typing import List, Optional, Union -from ..api import InteractionException +from ..api.error import LibraryException from ..api.models.attrs_utils import MISSING, DictSerializerMixin, define, field from ..api.models.channel import Channel from ..api.models.guild import Guild @@ -483,7 +483,7 @@ async def func(): ): _choices = list(choices) else: - raise InteractionException( + raise LibraryException( 6, message="Autocomplete choice items must be of type Choice" )