diff --git a/discord_slash/client.py b/discord_slash/client.py index 6493c6b28..a92ad80e8 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -9,6 +9,15 @@ from . import context, error, http, model from .utils import manage_commands +from .utils.manage_components import get_components_ids, get_messages_ids + + +def _get_val(d: dict, key): # util function to get value from dict with fallback to None key + try: + value = d[key] + except KeyError: # if there is no specific key set, we fallback to "global/any" + value = d[None] + return value class SlashCommand: @@ -53,6 +62,7 @@ def __init__( self._discord = client self.commands = {} self.subcommands = {} + self.components = {} self.logger = logging.getLogger("discord_slash") self.req = http.SlashCommandRequest(self.logger, self._discord, application_id) self.sync_commands = sync_commands @@ -126,11 +136,17 @@ def get_cog_commands(self, cog: commands.Cog): ) cog._slash_registered = True # Assuming all went well func_list = [getattr(cog, x) for x in dir(cog)] + + self._get_cog_slash_commands(cog, func_list) + self._get_cog_component_callbacks(cog, func_list) + + def _get_cog_slash_commands(self, cog, func_list): res = [ x for x in func_list if isinstance(x, (model.CogBaseCommandObject, model.CogSubcommandObject)) ] + for x in res: x.cog = cog if isinstance(x, model.CogBaseCommandObject): @@ -170,6 +186,13 @@ def get_cog_commands(self, cog: commands.Cog): raise error.DuplicateCommand(f"{x.base} {x.name}") self.subcommands[x.base][x.name] = x + def _get_cog_component_callbacks(self, cog, func_list): + res = [x for x in func_list if isinstance(x, model.CogComponentCallbackObject)] + + for x in res: + x.cog = cog + self._add_comp_callback_obj(x) + def remove_cog_commands(self, cog): """ Removes slash command from :class:`discord.ext.commands.Cog`. @@ -183,6 +206,10 @@ def remove_cog_commands(self, cog): if hasattr(cog, "_slash_registered"): del cog._slash_registered func_list = [getattr(cog, x) for x in dir(cog)] + self._remove_cog_slash_commands(func_list) + self._remove_cog_component_callbacks(func_list) + + def _remove_cog_slash_commands(self, func_list): res = [ x for x in func_list @@ -213,6 +240,12 @@ def remove_cog_commands(self, cog): else: del self.commands[x.base] + def _remove_cog_component_callbacks(self, func_list): + res = [x for x in func_list if isinstance(x, model.CogComponentCallbackObject)] + + for x in res: + self.remove_component_callback_obj(x) + async def to_dict(self): """ Converts all commands currently registered to :class:`SlashCommand` to a dictionary. @@ -858,6 +891,202 @@ def wrapper(cmd): return wrapper + def add_component_callback( + self, + callback: typing.Coroutine, + *, + messages: typing.Union[int, discord.Message, list] = None, + components: typing.Union[str, dict, list] = None, + use_callback_name=True, + component_type: int = None, + ): + """ + Adds a coroutine callback to a component. + Callback can be made to only accept component interactions from a specific messages + and/or custom_ids of components. + + :param Coroutine callback: The coroutine to be called when the component is interacted with. Must accept a single argument with the type :class:`.context.ComponentContext`. + :param messages: If specified, only interactions from the message given will be accepted. Can be a message object to check for, or the message ID or list of previous two. Empty list will mean that no interactions are accepted. + :type messages: Union[discord.Message, int, list] + :param components: If specified, only interactions with ``custom_id``s of given components will be accepted. Defaults to the name of ``callback`` if ``use_callback_name=True``. Can be a custom ID (str) or component dict (actionrow or button) or list of previous two. + :type components: Union[str, dict, list] + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `messages`` or ``components`` must be specified. + :type use_callback_name: bool + :param component_type: The type of the component to avoid collisions with other component types. See :class:`.model.ComponentType`. + :type component_type: Optional[int] + :raises: .error.DuplicateCustomID, .error.IncorrectFormat + """ + + message_ids = list(get_messages_ids(messages)) if messages is not None else [None] + custom_ids = list(get_components_ids(components)) if components is not None else [None] + + if use_callback_name and custom_ids == [None]: + custom_ids = [callback.__name__] + + if message_ids == [None] and custom_ids == [None]: + raise error.IncorrectFormat("You must specify messages or components (or both)") + + callback_obj = model.ComponentCallbackObject( + callback, message_ids, custom_ids, component_type + ) + self._add_comp_callback_obj(callback_obj) + return callback_obj + + def _add_comp_callback_obj(self, callback_obj): + component_type = callback_obj.component_type + + for message_id, custom_id in callback_obj.keys: + self._register_comp_callback_obj(callback_obj, message_id, custom_id, component_type) + + def _register_comp_callback_obj(self, callback_obj, message_id, custom_id, component_type): + message_id_dict = self.components + custom_id_dict = message_id_dict.setdefault(message_id, {}) + component_type_dict = custom_id_dict.setdefault(custom_id, {}) + + if component_type in component_type_dict: + raise error.DuplicateCallback(message_id, custom_id, component_type) + + component_type_dict[component_type] = callback_obj + self.logger.debug( + f"Added component callback for " + f"message ID {message_id or ''}, " + f"custom_id `{custom_id or ''}`, " + f"component_type `{component_type or ''}`" + ) + + def extend_component_callback( + self, + callback_obj: model.ComponentCallbackObject, + message_id: int = None, + custom_id: str = None, + ): + """ + Registers existing callback object (:class:`.model.ComponentType`) + for specific combination of message_id, custom_id, component_type. + + :param callback_obj: callback object. + :type callback_obj: model.ComponentCallbackObject + :param message_id: If specified, only removes the callback for the specific message ID. + :type message_id: Optional[.model] + :param custom_id: The `custom_id` of the component. + :type custom_id: Optional[str] + :raises: .error.DuplicateCustomID, .error.IncorrectFormat + """ + + component_type = callback_obj.component_type + self._register_comp_callback_obj(callback_obj, message_id, custom_id, component_type) + callback_obj.keys.add((message_id, custom_id)) + + def get_component_callback( + self, + message_id: int = None, + custom_id: str = None, + component_type: int = None, + ): + """ + Returns component callback (or None if not found) for specific combination of message_id, custom_id, component_type. + + :param message_id: If specified, only removes the callback for the specific message ID. + :type message_id: Optional[.model] + :param custom_id: The `custom_id` of the component. + :type custom_id: Optional[str] + :param component_type: The type of the component. See :class:`.model.ComponentType`. + :type component_type: Optional[int] + + :return: Optional[model.ComponentCallbackObject] + """ + message_id_dict = self.components + try: + custom_id_dict = _get_val(message_id_dict, message_id) + component_type_dict = _get_val(custom_id_dict, custom_id) + callback = _get_val(component_type_dict, component_type) + + except KeyError: # there was no key in dict and no global fallback + pass + else: + return callback + + def remove_component_callback( + self, message_id: int = None, custom_id: str = None, component_type: int = None + ): + """ + Removes a component callback from specific combination of message_id, custom_id, component_type. + + :param message_id: If specified, only removes the callback for the specific message ID. + :type message_id: Optional[int] + :param custom_id: The `custom_id` of the component. + :type custom_id: Optional[str] + :param component_type: The type of the component. See :class:`.model.ComponentType`. + :type component_type: Optional[int] + :raises: .error.IncorrectFormat + """ + try: + callback = self.components[message_id][custom_id].pop(component_type) + if not self.components[message_id][custom_id]: # delete dict nesting levels if empty + self.components[message_id].pop(custom_id) + if not self.components[message_id]: + self.components.pop(message_id) + except KeyError: + raise error.IncorrectFormat( + f"Callback for " + f"message ID `{message_id or ''}`, " + f"custom_id `{custom_id or ''}`, " + f"component_type `{component_type or ''}` is not registered!" + ) + else: + callback.keys.remove((message_id, custom_id)) + + def remove_component_callback_obj(self, callback_obj: model.ComponentCallbackObject): + """ + Removes a component callback from all related message_id, custom_id listeners. + + :param callback_obj: callback object. + :type callback_obj: model.ComponentCallbackObject + :raises: .error.IncorrectFormat + """ + if not callback_obj.keys: + raise error.IncorrectFormat("Callback already removed from any listeners") + + component_type = callback_obj.component_type + for message_id, custom_id in callback_obj.keys.copy(): + self.remove_component_callback(message_id, custom_id, component_type) + + def component_callback( + self, + *, + messages: typing.Union[int, discord.Message, list] = None, + components: typing.Union[str, dict, list] = None, + use_callback_name=True, + component_type: int = None, + ): + """ + Decorator that registers a coroutine as a component callback. + Adds a coroutine callback to a component. + Callback can be made to only accept component interactions from a specific messages + and/or custom_ids of components. + + :param messages: If specified, only interactions from the message given will be accepted. Can be a message object to check for, or the message ID or list of previous two. Empty list will mean that no interactions are accepted. + :type messages: Union[discord.Message, int, list] + :param components: If specified, only interactions with ``custom_id``s of given components will be accepted. Defaults to the name of ``callback`` if ``use_callback_name=True``. Can be a custom ID (str) or component dict (actionrow or button) or list of previous two. + :type components: Union[str, dict, list] + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `messages`` or ``components`` must be specified. + :type use_callback_name: bool + :param component_type: The type of the component to avoid collisions with other component types. See :class:`.model.ComponentType`. + :type component_type: Optional[int] + :raises: .error.DuplicateCustomID, .error.IncorrectFormat + """ + + def wrapper(callback): + return self.add_component_callback( + callback, + messages=messages, + components=components, + use_callback_name=use_callback_name, + component_type=component_type, + ) + + return wrapper + async def process_options( self, guild: discord.Guild, @@ -958,17 +1187,34 @@ async def invoke_command(self, func, ctx, args): else: await func.invoke(ctx, *args) except Exception as ex: - if hasattr(func, "on_error"): - if func.on_error is not None: - try: - if hasattr(func, "cog"): - await func.on_error(func.cog, ctx, ex) - else: - await func.on_error(ctx, ex) - return - except Exception as e: - self.logger.error(f"{ctx.command}:: Error using error decorator: {e}") - await self.on_slash_command_error(ctx, ex) + if not await self._handle_invoke_error(func, ctx, ex): + await self.on_slash_command_error(ctx, ex) + + async def invoke_component_callback(self, func, ctx): + """ + Invokes component callback. + + :param func: Component callback object. + :param ctx: Context. + """ + try: + await func.invoke(ctx) + except Exception as ex: + if not await self._handle_invoke_error(func, ctx, ex): + await self.on_component_callback_error(ctx, ex) + + async def _handle_invoke_error(self, func, ctx, ex): + if hasattr(func, "on_error"): + if func.on_error is not None: + try: + if hasattr(func, "cog"): + await func.on_error(func.cog, ctx, ex) + else: + await func.on_error(ctx, ex) + return True + except Exception as e: + self.logger.error(f"{ctx.command}:: Error using error decorator: {e}") + return False async def on_socket_response(self, msg): """ @@ -995,6 +1241,13 @@ async def _on_component(self, to_use): ctx = context.ComponentContext(self.req, to_use, self._discord, self.logger) self._discord.dispatch("component", ctx) + callback = self.get_component_callback( + ctx.origin_message_id, ctx.custom_id, ctx.component_type + ) + if callback is not None: + self._discord.dispatch("component_callback", ctx, callback) + await self.invoke_component_callback(callback, ctx) + async def _on_slash(self, to_use): if to_use["data"]["name"] in self.commands: @@ -1102,6 +1355,17 @@ async def handle_subcommand(self, ctx: context.SlashContext, data: dict): self._discord.dispatch("slash_command", ctx) await self.invoke_command(selected, ctx, args) + def _on_error(self, ctx, ex, event_name): + on_event = "on_" + event_name + if self.has_listener: + if self._discord.extra_events.get(on_event): + self._discord.dispatch(event_name, ctx, ex) + return True + if hasattr(self._discord, on_event): + self._discord.dispatch(event_name, ctx, ex) + return True + return False + async def on_slash_command_error(self, ctx, ex): """ Handles Exception occurred from invoking command. @@ -1128,12 +1392,40 @@ async def on_slash_command_error(ctx, ex): :type ex: Exception :return: """ - if self.has_listener: - if self._discord.extra_events.get("on_slash_command_error"): - self._discord.dispatch("slash_command_error", ctx, ex) - return - if hasattr(self._discord, "on_slash_command_error"): - self._discord.dispatch("slash_command_error", ctx, ex) - return - # Prints exception if not overridden or has no listener for error. - self.logger.exception(f"An exception has occurred while executing command `{ctx.name}`:") + if not self._on_error(ctx, ex, "slash_command_error"): + # Prints exception if not overridden or has no listener for error. + self.logger.exception( + f"An exception has occurred while executing command `{ctx.name}`:" + ) + + async def on_component_callback_error(self, ctx, ex): + """ + Handles Exception occurred from invoking component callback. + + Example of adding event: + + .. code-block:: python + + @client.event + async def on_component_callback_error(ctx, ex): + ... + + Example of adding listener: + + .. code-block:: python + + @bot.listen() + async def on_component_callback_error(ctx, ex): + ... + + :param ctx: Context of the callback. + :type ctx: :class:`.model.ComponentContext` + :param ex: Exception from the command invoke. + :type ex: Exception + :return: + """ + if not self._on_error(ctx, ex, "component_callback_error"): + # Prints exception if not overridden or has no listener for error. + self.logger.exception( + f"An exception has occurred while executing component callback custom ID `{ctx.custom_id}`:" + ) diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index 7f3f9c75e..7ad26c93c 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -1,9 +1,12 @@ import inspect import typing -from .error import IncorrectGuildIDType -from .model import CogBaseCommandObject, CogSubcommandObject +import discord + +from .error import IncorrectFormat, IncorrectGuildIDType +from .model import CogBaseCommandObject, CogComponentCallbackObject, CogSubcommandObject from .utils import manage_commands +from .utils.manage_components import get_components_ids, get_messages_ids def cog_slash( @@ -18,7 +21,7 @@ def cog_slash( ): """ Decorator for Cog to add slash command.\n - Almost same as :func:`.client.SlashCommand.slash`. + Almost same as :meth:`.client.SlashCommand.slash`. Example: @@ -94,7 +97,7 @@ def cog_subcommand( ): """ Decorator for Cog to add subcommand.\n - Almost same as :func:`.client.SlashCommand.subcommand`. + Almost same as :meth:`.client.SlashCommand.subcommand`. Example: @@ -174,3 +177,46 @@ def wrapper(cmd): return CogSubcommandObject(base, _cmd, subcommand_group, name or cmd.__name__, _sub) return wrapper + + +def cog_component( + *, + messages: typing.Union[int, discord.Message, list] = None, + components: typing.Union[str, dict, list] = None, + use_callback_name=True, + component_type: int = None, +): + """ + Decorator for component callbacks in cogs.\n + Almost same as :meth:`.client.SlashCommand.component_callback`. + + :param messages: If specified, only interactions from the message given will be accepted. Can be a message object to check for, or the message ID or list of previous two. Empty list will mean that no interactions are accepted. + :type messages: Union[discord.Message, int, list] + :param components: If specified, only interactions with ``custom_id``s of given components will be accepted. Defaults to the name of ``callback`` if ``use_callback_name=True``. Can be a custom ID (str) or component dict (actionrow or button) or list of previous two. + :type components: Union[str, dict, list] + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `messages`` or ``components`` must be specified. + :type use_callback_name: bool + :param component_type: The type of the component to avoid collisions with other component types. See :class:`.model.ComponentType`. + :type component_type: Optional[int] + :raises: .error.DuplicateCustomID, .error.IncorrectFormat + """ + message_ids = list(get_messages_ids(messages)) if messages is not None else [None] + custom_ids = list(get_components_ids(components)) if components is not None else [None] + + def wrapper(callback): + nonlocal custom_ids + + if use_callback_name and custom_ids == [None]: + custom_ids = [callback.__name__] + + if message_ids == [None] and custom_ids == [None]: + raise IncorrectFormat("You must specify messages or components (or both)") + + return CogComponentCallbackObject( + callback, + message_ids=message_ids, + custom_ids=custom_ids, + component_type=component_type, + ) + + return wrapper diff --git a/discord_slash/error.py b/discord_slash/error.py index 30e6041ed..d09875e48 100644 --- a/discord_slash/error.py +++ b/discord_slash/error.py @@ -40,6 +40,20 @@ def __init__(self, name: str): super().__init__(f"Duplicate command name detected: {name}") +class DuplicateCallback(SlashCommandError): + """ + There is a duplicate component callback. + """ + + def __init__(self, message_id: int, custom_id: str, component_type: int): + super().__init__( + f"Duplicate component callback detected: " + f"message ID {message_id or ''}, " + f"custom_id `{custom_id or ''}`, " + f"component_type `{component_type or ''}`" + ) + + class DuplicateSlashClient(SlashCommandError): """ There are duplicate :class:`.SlashCommand` instances. diff --git a/discord_slash/model.py b/discord_slash/model.py index 5de1e0ce2..bcf2f2b42 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -87,7 +87,7 @@ def __init__( id=None, application_id=None, version=None, - **kwargs + **kwargs, ): self.name = name self.description = description @@ -114,29 +114,20 @@ def __eq__(self, other): return False -class CommandObject: +class CallbackObject: """ - Slash command object of this extension. + Callback object of this extension. .. warning:: Do not manually init this model. - :ivar name: Name of the command. :ivar func: The coroutine of the command. - :ivar description: Description of the command. - :ivar allowed_guild_ids: List of the allowed guild id. - :ivar options: List of the option of the command. Used for `auto_register`. - :ivar connector: Kwargs connector of the command. :ivar __commands_checks__: Check of the command. """ - def __init__(self, name, cmd): # Let's reuse old command formatting. - self.name = name.lower() - self.func = cmd["func"] - self.description = cmd["description"] - self.allowed_guild_ids = cmd["guild_ids"] or [] - self.options = cmd["api_options"] or [] - self.connector = cmd["connector"] or {} + def __init__(self, func): + self.func = func + # Ref https://github.com/Rapptz/discord.py/blob/master/discord/ext/commands/core.py#L1447 # Since this isn't inherited from `discord.ext.commands.Command`, discord.py's check decorator will # add checks at this var. @@ -300,6 +291,29 @@ async def can_run(self, ctx) -> bool: return False not in res +class CommandObject(CallbackObject): + """ + Slash command object of this extension. + + .. warning:: + Do not manually init this model. + + :ivar name: Name of the command. + :ivar description: Description of the command. + :ivar allowed_guild_ids: List of the allowed guild id. + :ivar options: List of the option of the command. Used for `auto_register`. + :ivar connector: Kwargs connector of the command. + """ + + def __init__(self, name, cmd): # Let's reuse old command formatting. + super().__init__(cmd["func"]) + self.name = name.lower() + self.description = cmd["description"] + self.allowed_guild_ids = cmd["guild_ids"] or [] + self.options = cmd["api_options"] or [] + self.connector = cmd["connector"] or {} + + class BaseCommandObject(CommandObject): """ BaseCommand object of this extension. @@ -373,6 +387,59 @@ def __init__(self, base, cmd, sub_group, name, sub): self.cog = None # Manually set this later. +class ComponentCallbackObject(CallbackObject): + """ + Internal component object. Inherits :class:`CallbackObject`, so it has all variables from it. + + .. warning:: + Do not manually init this model. + + :ivar message_ids: The message IDs registered to this callback. + :ivar custom_ids: The component custom IDs registered to this callback. + :ivar component_type: Type of the component. See `:class.utils.manage_components.ComponentsType` + """ + + def __init__( + self, + func, + message_ids, + custom_ids, + component_type, + ): + if component_type not in (2, 3, None): + raise error.IncorrectFormat(f"Invalid component type `{component_type}`") + + super().__init__(func) + message_ids = set(message_ids) + custom_ids = set(custom_ids) + self.keys = { + (message_id, custom_id) for message_id in message_ids for custom_id in custom_ids + } + + self.component_type = component_type + + async def invoke(self, ctx): + """ + Invokes the component callback. + + :param ctx: The :class:`.context.ComponentContext` for the interaction. + """ + return await super().invoke(ctx) + + +class CogComponentCallbackObject(ComponentCallbackObject): + """ + Component callback object but for Cog. Has all variables from :class:`ComponentCallbackObject`. + + .. warning:: + Do not manually init this model. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.cog = None # Manually set this later. + + class SlashCommandOptionType(IntEnum): """ Equivalent of `ApplicationCommandOptionType `_ in the Discord API. diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 60f0bfbd1..7ae264179 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -26,6 +26,51 @@ def create_actionrow(*components: dict) -> dict: return {"type": ComponentType.actionrow, "components": components} +def spread_to_rows(*components, max_in_row=5) -> typing.List[dict]: + """ + Generates list of actionsrows from given components. + + :param components: Components dicts (buttons or selects or existing actionrows) to spread. Use `None` to explicitly start a new row. + :type components: dict + :param max_in_row: Maximum number of elements in each row. + :type max_in_row: int + :return: list + """ + if not components or len(components) > 25: + raise IncorrectFormat("Number of components should be between 1 and 25.") + + if max_in_row < 1 or max_in_row > 5: + raise IncorrectFormat("max_in_row should be between 1 and 5.") + + rows = [] + button_row = [] + for component in list(components) + [None]: + if component is not None and component["type"] is ComponentType.button: + button_row.append(component) + + if len(button_row) == max_in_row: + rows.append(create_actionrow(*button_row)) + button_row = [] + + continue + + if button_row: + rows.append(create_actionrow(*button_row)) + button_row = [] + + if component is None: + pass + elif component["type"] is ComponentType.actionrow: + rows.append(component) + elif component["type"] is ComponentType.select: + rows.append(create_actionrow(component)) + + if len(rows) > 5: + raise IncorrectFormat("Number of rows exceeds 5.") + + return rows + + def emoji_to_dict(emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str]) -> dict: """ Converts a default or custom emoji into a partial emoji dict. @@ -152,9 +197,10 @@ def create_select( def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Iterator[str]: """ - Returns generator with 'custom_id' of component or components. + Returns generator with 'custom_id' of component or list of components. :param component: Custom ID or component dict (actionrow or button) or list of previous two. + :returns: typing.Iterator[str] """ if isinstance(component, str): @@ -174,13 +220,19 @@ def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Itera ) -def _get_messages_ids(message: typing.Union[discord.Message, int, list]) -> typing.Iterator[int]: +def get_messages_ids(message: typing.Union[int, discord.Message, list]) -> typing.Iterator[int]: + """ + Returns generator with id of message or list messages. + + :param message: message ID or message object or list of previous two. + :returns: typing.Iterator[int] + """ if isinstance(message, int): yield message elif isinstance(message, discord.Message): yield message.id elif isinstance(message, list): - yield from (msg_id for msg in message for msg_id in _get_messages_ids(msg)) + yield from (msg_id for msg in message for msg_id in get_messages_ids(msg)) else: raise IncorrectType( f"Unknown component type of {message} ({type(message)}). " @@ -190,8 +242,8 @@ def _get_messages_ids(message: typing.Union[discord.Message, int, list]) -> typi async def wait_for_component( client: discord.Client, - component: typing.Union[str, dict, list] = None, - message: typing.Union[discord.Message, int, list] = None, + messages: typing.Union[discord.Message, int, list] = None, + components: typing.Union[str, dict, list] = None, check=None, timeout=None, ) -> ComponentContext: @@ -201,26 +253,27 @@ async def wait_for_component( :param client: The client/bot object. :type client: :class:`discord.Client` - :param component: Custom ID or component dict (actionrow or button) or list of previous two. - :param message: The message object to check for, or the message ID or list of previous two. - :type component: Union[dict, str] + :param messages: The message object to check for, or the message ID or list of previous two. + :type messages: Union[discord.Message, int, list] + :param components: Custom ID to check for, or component dict (actionrow or button) or list of previous two. + :type components: Union[str, dict, list] :param check: Optional check function. Must take a `ComponentContext` as the first parameter. :param timeout: The number of seconds to wait before timing out and raising :exc:`asyncio.TimeoutError`. :raises: :exc:`asyncio.TimeoutError` """ - if not (component or message): - raise IncorrectFormat("You must specify component or message (or both)") + if not (messages or components): + raise IncorrectFormat("You must specify messages or components (or both)") - components_ids = list(get_components_ids(component)) if component else None - message_ids = list(_get_messages_ids(message)) if message else None + message_ids = list(get_messages_ids(messages)) if messages else None + custom_ids = list(get_components_ids(components)) if components else None def _check(ctx: ComponentContext): if check and not check(ctx): return False - # if components_ids is empty or there is a match - wanted_component = not components_ids or ctx.custom_id in components_ids + # if custom_ids is empty or there is a match wanted_message = not message_ids or ctx.origin_message_id in message_ids - return wanted_component and wanted_message + wanted_component = not custom_ids or ctx.custom_id in custom_ids + return wanted_message and wanted_component return await client.wait_for("component", check=_check, timeout=timeout) diff --git a/docs/events.rst b/docs/events.rst index f695fd5e4..a591ff943 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -27,3 +27,20 @@ These events can be registered to discord.py's listener or :param ctx: ComponentContext of the triggered component. :type ctx: :class:`.model.ComponentContext` +.. function:: on_component_callback(ctx, callback) + + Called when a component callback is triggered. + + :param ctx: ComponentContext of the triggered component. + :type ctx: :class:`.model.ComponentContext` + :param callback: triggered ComponentCallbackObject + :type callback: :class:`.model.ComponentCallbackObject` + +.. function:: on_component_callback_error(ctx, ex) + + Called when component callback had an exception while the callback was invoked. + + :param ctx: Context of the callback. + :type ctx: :class:`.model.ComponentContext` + :param ex: Exception from the command invoke. + :type ex: Exception \ No newline at end of file