From 6e8a186e91293e5242e4b951d86e8db513653f0e Mon Sep 17 00:00:00 2001 From: hpenney2 Date: Mon, 7 Jun 2021 19:43:21 -0500 Subject: [PATCH 01/15] Add support for component callbacks --- discord_slash/client.py | 127 +++++++++++++++++++++++++++++++++++++++- discord_slash/error.py | 8 +++ discord_slash/model.py | 31 ++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 840565647..69be3989a 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -8,7 +8,7 @@ from discord.ext import commands from . import context, error, http, model -from .utils import manage_commands +from .utils import manage_commands, manage_components class SlashCommand: @@ -53,6 +53,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 @@ -847,6 +848,128 @@ def wrapper(cmd): return wrapper + def add_component_callback(self, callback: typing.Coroutine, + component_type: int, + custom_id: str = None, + message_id: typing.Optional[int] = None): + """ + Adds a coroutine callback to a component. Optionally, this can be made to only accept component interactions from a specific message. + + :param callback: The coroutine to be called when the component is interacted with. Must accept a single argument with the type :class:`.context.ComponentContext`. + :type callback: Coroutine + :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :type component_type: int + :param custom_id: The `custom_id` of the component. Defaults to the name of `callback`. + :type custom_id: str + :param message_id: If specified, only interactions from the message given will be accepted. + :type message_id: Optional[int] + :raises: .error.DuplicateCustomID + """ + if 2 > component_type > 3: + raise error.IncorrectFormat(f"Invalid component type `{component_type}`") + + custom_id = custom_id or callback.__name__ + key = (custom_id, component_type) + + if message_id: + if key in self.components and self.components[key].funcList.get(message_id): + raise error.DuplicateCustomID(custom_id) + elif key in self.components: + obj = self.components[key] + obj.funcList[message_id] = callback + else: + obj = model.ComponentCallbackObject(custom_id, funcList={message_id: callback}) + self.components[key] = obj + + self.logger.debug(f"Added message-specific component callback for `{custom_id}`, message ID {message_id}") + else: + if key in self.components and self.components[key].func: + raise error.DuplicateCustomID(custom_id) + obj = model.ComponentCallbackObject(custom_id, func=callback) + self.components[key] = obj + self.logger.debug(f"Added component callback for `{custom_id}`") + + return obj + + def remove_component_callback(self, custom_id: str, component_type: int, message_id: typing.Optional[int] = None): + """ + Removes a component callback. If the `message_id` is specified, only removes the callback for the specific message ID. + + :param custom_id: The `custom_id` of the component. + :type custom_id: str + :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :type component_type: int + :param message_id: If specified, only removes the callback for the specific message ID. + :type message_id: Optional[int] + :raises: .error.IncorrectFormat + """ + if 2 > component_type > 3: + raise error.IncorrectFormat(f"Invalid component type `{component_type}`") + + key = (custom_id, component_type) + + if message_id: + if key in self.components and self.components[key].funcs.get(message_id): + obj = self.components[key] + del obj.funcList[message_id] + if len(obj.funcList) == 0 and not obj.func: + del self.components[key] + elif custom_id in self.components: + raise error.IncorrectFormat(f"Message ID `{message_id}` is not registered to custom ID `{custom_id}`!") + else: + raise error.IncorrectFormat(f"Custom ID `{custom_id}` is not registered as a message-specific " + f"component!") + else: + if key in self.components: + del self.components[key] + else: + raise error.IncorrectFormat(f"Custom ID `{custom_id}` is not registered as a component!") + + def component_callback(self, + component_type: typing.Union[int, manage_components.ComponentsType], + custom_id: str = None, + *, + message_id: typing.Optional[int] = None, + message_ids: typing.Optional[typing.List[int]] = None): + """ + Decorator that registers a coroutine as a component callback.\n + The second argument is the `custom_id` to listen for. + It will default to the coroutine name if unspecified.\n + The `message_id` keyword-only arg is optional, + but will make the callback only work with a specific message if given.\n + Alternatively, if it needs to accept interactions from multiple specific messages, the `message_ids` arg + accepts a list of message IDs. + + .. note:: + `message_id` and `message_ids` cannot be used at the same time. + + :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :type component_type: Union[int, manage_components.ComponentsType] + :param custom_id: The `custom_id` of the component. Defaults to the name of coroutine being decorated. + :type custom_id: str + :param message_id: If specified, only interactions from the message given will be accepted. + :type message_id: Optional[int] + :param message_ids: Acts like `message_id`, but accepts a list of message IDs instead. + :type message_ids: Optional[List[int]] + :raises: .error.IncorrectFormat + """ + if message_id and message_ids: + raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") + + if isinstance(component_type, manage_components.ComponentsType): + component_type = component_type.value + + def wrapper(callback): + if message_ids: + for msg in message_ids: + self.add_component_callback(callback, component_type, custom_id, msg) + elif message_id: + self.add_component_callback(callback, component_type, custom_id, message_id) + else: + self.add_component_callback(callback, component_type, custom_id) + + return wrapper + async def process_options( self, guild: discord.Guild, @@ -983,6 +1106,8 @@ async def on_socket_response(self, msg): async def _on_component(self, to_use): ctx = context.ComponentContext(self.req, to_use, self._discord, self.logger) self._discord.dispatch("component", ctx) + if (ctx.custom_id, ctx.component_type) in self.components: + return await self.components[(ctx.custom_id, ctx.component_type)].invoke(ctx) async def _on_slash(self, to_use): if to_use["data"]["name"] in self.commands: diff --git a/discord_slash/error.py b/discord_slash/error.py index 897576281..a6788c7f3 100644 --- a/discord_slash/error.py +++ b/discord_slash/error.py @@ -40,6 +40,14 @@ def __init__(self, name: str): super().__init__(f"Duplicate command name detected: {name}") +class DuplicateCustomID(SlashCommandError): + """ + There is a duplicate component custom ID. + """ + def __init__(self, custom_id: str): + super().__init__(f"Duplicate component custom ID detected: {custom_id}") + + class DuplicateSlashClient(SlashCommandError): """ There are duplicate :class:`.SlashCommand` instances. diff --git a/discord_slash/model.py b/discord_slash/model.py index c8b4f2a34..3a93b3759 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -366,6 +366,37 @@ def __init__(self, base, cmd, sub_group, name, sub): self.cog = None # Manually set this later. +class ComponentCallbackObject: + """ + Internal component object. + + .. warning:: + Do not manually init this model. + + :ivar custom_id: Custom ID of the component. + :ivar func: An optional single callback coroutine for the component. If the message ID given isn't in ``funcList``, this function will be ran instead. + :ivar funcList: An optional :class:`dict` with message IDs as keys and callback coroutines as values. If a message ID is found in the dict, the corresponding coroutine will be ran. + """ + def __init__(self, custom_id, func=None, funcList=None): + if funcList is None: + funcList = {} + self.custom_id = custom_id + self.func = func + self.funcList = funcList + + async def invoke(self, ctx): + """ + Invokes the component callback. + + :param ctx: The :class:`.context.ComponentContext` for the interaction. + """ + if self.funcList and self.funcList.get(ctx.origin_message_id): + coro = self.funcList.get(ctx.origin_message_id) + return await coro(ctx) + elif self.func: + return await self.func(ctx) + + class SlashCommandOptionType(IntEnum): """ Equivalent of `ApplicationCommandOptionType `_ in the Discord API. From 86d4d60c4be10f81f5665bb60ad54cdbc39136af Mon Sep 17 00:00:00 2001 From: Artem Date: Fri, 11 Jun 2021 21:08:51 +0300 Subject: [PATCH 02/15] Refactored component callbacks. Added cog callbacks support --- discord_slash/client.py | 229 +++++++++++++++++++++++++++------------ discord_slash/cog_ext.py | 52 ++++++++- discord_slash/error.py | 12 +- discord_slash/model.py | 81 +++++++++----- 4 files changed, 275 insertions(+), 99 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 69be3989a..a333f253b 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -11,6 +11,14 @@ from .utils import manage_commands, manage_components +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: """ Slash command handler class. @@ -126,11 +134,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 +184,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 +204,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 +238,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. @@ -848,50 +879,102 @@ def wrapper(cmd): return wrapper - def add_component_callback(self, callback: typing.Coroutine, - component_type: int, - custom_id: str = None, - message_id: typing.Optional[int] = None): + def add_component_callback( + self, + callback: typing.Coroutine, + *, + message_id: int = None, + message_ids: typing.List[int] = None, + custom_id: str = None, + custom_ids: typing.List[str] = None, + use_callback_name=True, + component_type: int = None, + ): """ Adds a coroutine callback to a component. Optionally, this can be made to only accept component interactions from a specific message. :param callback: The coroutine to be called when the component is interacted with. Must accept a single argument with the type :class:`.context.ComponentContext`. :type callback: Coroutine - :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. - :type component_type: int :param custom_id: The `custom_id` of the component. Defaults to the name of `callback`. :type custom_id: str + :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :type component_type: int :param message_id: If specified, only interactions from the message given will be accepted. :type message_id: Optional[int] :raises: .error.DuplicateCustomID """ - if 2 > component_type > 3: - raise error.IncorrectFormat(f"Invalid component type `{component_type}`") + if message_id and message_ids: + raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") - custom_id = custom_id or callback.__name__ - key = (custom_id, component_type) + if custom_id and custom_ids: + raise error.IncorrectFormat("You cannot use both `custom_id` and `custom_ids`!") if message_id: - if key in self.components and self.components[key].funcList.get(message_id): - raise error.DuplicateCustomID(custom_id) - elif key in self.components: - obj = self.components[key] - obj.funcList[message_id] = callback - else: - obj = model.ComponentCallbackObject(custom_id, funcList={message_id: callback}) - self.components[key] = obj + message_ids = [message_id] - self.logger.debug(f"Added message-specific component callback for `{custom_id}`, message ID {message_id}") - else: - if key in self.components and self.components[key].func: - raise error.DuplicateCustomID(custom_id) - obj = model.ComponentCallbackObject(custom_id, func=callback) - self.components[key] = obj - self.logger.debug(f"Added component callback for `{custom_id}`") + if custom_id: + custom_ids = [custom_id] - return obj + if use_callback_name: + custom_ids = custom_ids or [callback.__name__] + + if not (message_ids or custom_ids): + raise error.IncorrectFormat( + "'message_ids' ('message_id') or 'custom_ids' ('custom_id') must be specified!" + ) + + if custom_id: + custom_ids = [custom_id] + + if use_callback_name: + custom_ids = custom_ids or [callback.__name__] + + 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 - def remove_component_callback(self, custom_id: str, component_type: int, message_id: typing.Optional[int] = None): + for message_id in callback_obj.message_ids: + for custom_id in callback_obj.custom_ids: + 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 get_component_callback( + self, + message_id: int = None, + custom_id: str = None, + component_type: int = None, + ): + 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. If the `message_id` is specified, only removes the callback for the specific message ID. @@ -903,34 +986,43 @@ def remove_component_callback(self, custom_id: str, component_type: int, message :type message_id: Optional[int] :raises: .error.IncorrectFormat """ - if 2 > component_type > 3: - raise error.IncorrectFormat(f"Invalid component type `{component_type}`") + try: + 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!" + ) - key = (custom_id, component_type) + def remove_component_callback_obj(self, callback_obj: model.ComponentCallbackObject): + """ + Removes a component callback. - if message_id: - if key in self.components and self.components[key].funcs.get(message_id): - obj = self.components[key] - del obj.funcList[message_id] - if len(obj.funcList) == 0 and not obj.func: - del self.components[key] - elif custom_id in self.components: - raise error.IncorrectFormat(f"Message ID `{message_id}` is not registered to custom ID `{custom_id}`!") - else: - raise error.IncorrectFormat(f"Custom ID `{custom_id}` is not registered as a message-specific " - f"component!") - else: - if key in self.components: - del self.components[key] - else: - raise error.IncorrectFormat(f"Custom ID `{custom_id}` is not registered as a component!") - - def component_callback(self, - component_type: typing.Union[int, manage_components.ComponentsType], - custom_id: str = None, - *, - message_id: typing.Optional[int] = None, - message_ids: typing.Optional[typing.List[int]] = None): + :param callback_obj: callback object. + :type callback_obj: model.ComponentCallbackObject + :raises: .error.IncorrectFormat + """ + component_type = callback_obj.component_type + for message_id in callback_obj.message_ids: + for custom_id in callback_obj.custom_ids: + self.remove_component_callback(message_id, custom_id, component_type) + + def component_callback( + self, + *, + message_id: int = None, + message_ids: typing.List[int] = None, + custom_id: str = None, + custom_ids: typing.List[str] = None, + use_callback_name=True, + component_type: int = None, + ): """ Decorator that registers a coroutine as a component callback.\n The second argument is the `custom_id` to listen for. @@ -953,20 +1045,17 @@ def component_callback(self, :type message_ids: Optional[List[int]] :raises: .error.IncorrectFormat """ - if message_id and message_ids: - raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") - - if isinstance(component_type, manage_components.ComponentsType): - component_type = component_type.value def wrapper(callback): - if message_ids: - for msg in message_ids: - self.add_component_callback(callback, component_type, custom_id, msg) - elif message_id: - self.add_component_callback(callback, component_type, custom_id, message_id) - else: - self.add_component_callback(callback, component_type, custom_id) + return self.add_component_callback( + callback, + message_id=message_id, + message_ids=message_ids, + custom_id=custom_id, + custom_ids=custom_ids, + use_callback_name=use_callback_name, + component_type=component_type, + ) return wrapper @@ -1106,8 +1195,14 @@ async def on_socket_response(self, msg): async def _on_component(self, to_use): ctx = context.ComponentContext(self.req, to_use, self._discord, self.logger) self._discord.dispatch("component", ctx) - if (ctx.custom_id, ctx.component_type) in self.components: - return await self.components[(ctx.custom_id, ctx.component_type)].invoke(ctx) + + callback = self.get_component_callback( + ctx.origin_message_id, ctx.custom_id, ctx.component_type + ) + print(callback) + print(self.components) + if callback is not None: + return await callback.invoke(ctx) async def _on_slash(self, to_use): if to_use["data"]["name"] in self.commands: diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index 54e699f1f..aaea41738 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -1,7 +1,8 @@ import inspect import typing -from .model import CogBaseCommandObject, CogSubcommandObject +from . import error +from .model import CogBaseCommandObject, CogComponentCallbackObject, CogSubcommandObject from .utils import manage_commands @@ -13,7 +14,7 @@ def cog_slash( options: typing.List[dict] = None, default_permission: bool = True, permissions: typing.Dict[int, list] = None, - connector: dict = None + connector: dict = None, ): """ Decorator for Cog to add slash command.\n @@ -83,7 +84,7 @@ def cog_subcommand( sub_group_desc: str = None, guild_ids: typing.List[int] = None, options: typing.List[dict] = None, - connector: dict = None + connector: dict = None, ): """ Decorator for Cog to add subcommand.\n @@ -161,3 +162,48 @@ def wrapper(cmd): return CogSubcommandObject(base, _cmd, subcommand_group, name or cmd.__name__, _sub) return wrapper + + +def cog_component( + *, + message_id: int = None, + message_ids: typing.List[int] = None, + custom_id: str = None, + custom_ids: typing.List[str] = None, + use_callback_name=True, + component_type: int = None, +): + if message_id and message_ids: + raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") + + if custom_id and custom_ids: + raise error.IncorrectFormat("You cannot use both `custom_id` and `custom_ids`!") + + if message_id: + message_ids = [message_id] + + if custom_id: + custom_ids = [custom_id] + + if component_type not in (2, 3, None): + raise error.IncorrectFormat(f"Invalid component type `{component_type}`") + + def wrapper(callback): + nonlocal custom_ids + + if use_callback_name: + custom_ids = custom_ids or [callback.__name__] + + if not (message_ids or custom_ids): + raise error.IncorrectFormat( + "'message_ids' ('message_id') or 'custom_ids' ('custom_id') must be specified!" + ) + + 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 a6788c7f3..8e55a457f 100644 --- a/discord_slash/error.py +++ b/discord_slash/error.py @@ -40,12 +40,18 @@ def __init__(self, name: str): super().__init__(f"Duplicate command name detected: {name}") -class DuplicateCustomID(SlashCommandError): +class DuplicateCallback(SlashCommandError): """ There is a duplicate component custom ID. """ - def __init__(self, custom_id: str): - super().__init__(f"Duplicate component custom ID detected: {custom_id}") + + def __init__(self, message_id: int, custom_id: str, component_type: int): + super().__init__( + f"Duplicate component detected: " + f"message ID {message_id or ''}, " + f"custom_id `{custom_id or ''}`, " + f"component_type `{component_type or ''}`" + ) class DuplicateSlashClient(SlashCommandError): diff --git a/discord_slash/model.py b/discord_slash/model.py index 3a93b3759..eaf3cfe91 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -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. @@ -293,6 +284,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. @@ -366,7 +380,7 @@ def __init__(self, base, cmd, sub_group, name, sub): self.cog = None # Manually set this later. -class ComponentCallbackObject: +class ComponentCallbackObject(CallbackObject): """ Internal component object. @@ -377,12 +391,18 @@ class ComponentCallbackObject: :ivar func: An optional single callback coroutine for the component. If the message ID given isn't in ``funcList``, this function will be ran instead. :ivar funcList: An optional :class:`dict` with message IDs as keys and callback coroutines as values. If a message ID is found in the dict, the corresponding coroutine will be ran. """ - def __init__(self, custom_id, func=None, funcList=None): - if funcList is None: - funcList = {} - self.custom_id = custom_id - self.func = func - self.funcList = funcList + + def __init__( + self, + func, + message_ids=None, + custom_ids=None, + component_type=None, + ): + super().__init__(func) + self.message_ids = message_ids or [None] + self.custom_ids = custom_ids or [None] + self.component_type = component_type async def invoke(self, ctx): """ @@ -390,11 +410,20 @@ async def invoke(self, ctx): :param ctx: The :class:`.context.ComponentContext` for the interaction. """ - if self.funcList and self.funcList.get(ctx.origin_message_id): - coro = self.funcList.get(ctx.origin_message_id) - return await coro(ctx) - elif self.func: - return await self.func(ctx) + return await super().invoke(ctx) + + +class CogComponentCallbackObject(ComponentCallbackObject): + """ + Component callback object but for Cog. + + .. 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): From bae9dd2f9fbf640d02096b0b2b554ee04192c3ef Mon Sep 17 00:00:00 2001 From: Artem Date: Fri, 11 Jun 2021 21:24:00 +0300 Subject: [PATCH 03/15] Added component_callback event dispatching, removed testing prints --- discord_slash/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index a333f253b..7914a6306 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -1199,10 +1199,9 @@ async def _on_component(self, to_use): callback = self.get_component_callback( ctx.origin_message_id, ctx.custom_id, ctx.component_type ) - print(callback) - print(self.components) if callback is not None: - return await callback.invoke(ctx) + self._discord.dispatch("component_callback", ctx, callback) + await callback.invoke(ctx) async def _on_slash(self, to_use): if to_use["data"]["name"] in self.commands: From 3705bed06db0f18da1592c6c3fadbd22bf981fe1 Mon Sep 17 00:00:00 2001 From: Artem Date: Fri, 11 Jun 2021 23:15:42 +0300 Subject: [PATCH 04/15] Added error handling event for component callbacks (same as for slash commands) --- discord_slash/client.py | 94 ++++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 21 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 7914a6306..00e1f699c 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -1159,17 +1159,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 command. + + :param func: Component callback coroutine. + :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): """ @@ -1201,7 +1218,7 @@ async def _on_component(self, to_use): ) if callback is not None: self._discord.dispatch("component_callback", ctx, callback) - await callback.invoke(ctx) + await self.invoke_component_callback(callback, ctx) async def _on_slash(self, to_use): if to_use["data"]["name"] in self.commands: @@ -1310,6 +1327,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. @@ -1336,12 +1364,36 @@ 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}`:") From 0afc918ba34c55238009a190f237c5656b70b25b Mon Sep 17 00:00:00 2001 From: Artem Date: Fri, 11 Jun 2021 23:16:48 +0300 Subject: [PATCH 05/15] Applied pre_push --- discord_slash/client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 00e1f699c..8bd9e49bd 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -8,7 +8,7 @@ from discord.ext import commands from . import context, error, http, model -from .utils import manage_commands, manage_components +from .utils import manage_commands def _get_val(d: dict, key): # util function to get value from dict with fallback to None key @@ -1328,7 +1328,7 @@ async def handle_subcommand(self, ctx: context.SlashContext, data: dict): await self.invoke_command(selected, ctx, args) def _on_error(self, ctx, ex, event_name): - on_event = "on_"+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) @@ -1366,7 +1366,9 @@ async def on_slash_command_error(ctx, ex): """ 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}`:") + self.logger.exception( + f"An exception has occurred while executing command `{ctx.name}`:" + ) async def on_component_callback_error(self, ctx, ex): """ @@ -1396,4 +1398,6 @@ async def on_component_callback_error(ctx, ex): """ 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}`:") + self.logger.exception( + f"An exception has occurred while executing component callback custom ID `{ctx.custom_id}`:" + ) From 5ccfc7b2cde10ea91a6d1924b6ca707bc9cad700 Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 12 Jun 2021 00:49:53 +0300 Subject: [PATCH 06/15] Fix for when remove_component_callback is used before remove_component_callback_obj --- discord_slash/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 8bd9e49bd..d7d56fae0 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -987,7 +987,7 @@ def remove_component_callback( :raises: .error.IncorrectFormat """ try: - self.components[message_id][custom_id].pop(component_type) + 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]: @@ -999,6 +999,9 @@ def remove_component_callback( f"custom_id `{custom_id or ''}`, " f"component_type `{component_type or ''}` is not registered!" ) + else: + callback.message_ids.remove(message_id) + callback.custom_ids.remove(custom_id) def remove_component_callback_obj(self, callback_obj: model.ComponentCallbackObject): """ From 77a7b7d4b6d85f59c057c9719c1fa487e09038ff Mon Sep 17 00:00:00 2001 From: hpenney2 Date: Fri, 11 Jun 2021 18:03:26 -0500 Subject: [PATCH 07/15] Update docs for changes --- discord_slash/client.py | 41 ++++++++++++++++++++++++++-------------- discord_slash/cog_ext.py | 25 ++++++++++++++++++++++-- discord_slash/model.py | 10 +++++----- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index d7d56fae0..4048058e8 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -893,14 +893,23 @@ def add_component_callback( """ Adds a coroutine callback to a component. Optionally, this can be made to only accept component interactions from a specific message. + .. note:: + ``message_id`` and ``message_ids`` cannot be used at the same time. The same applies to ``custom_id`` and ``custom_ids``. + :param callback: The coroutine to be called when the component is interacted with. Must accept a single argument with the type :class:`.context.ComponentContext`. :type callback: Coroutine - :param custom_id: The `custom_id` of the component. Defaults to the name of `callback`. - :type custom_id: str - :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. - :type component_type: int :param message_id: If specified, only interactions from the message given will be accepted. :type message_id: Optional[int] + :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. + :type message_ids: Optional[List[int]] + :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. + :type custom_id: Optional[str] + :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. + :type custom_ids: Optional[List[str]] + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. + :type use_callback_name: bool + :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :type component_type: Optional[int] :raises: .error.DuplicateCustomID """ if message_id and message_ids: @@ -979,9 +988,9 @@ def remove_component_callback( Removes a component callback. If the `message_id` is specified, only removes the callback for the specific message ID. :param custom_id: The `custom_id` of the component. - :type custom_id: str + :type custom_id: Optional[str] :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. - :type component_type: int + :type component_type: Optional[int] :param message_id: If specified, only removes the callback for the specific message ID. :type message_id: Optional[int] :raises: .error.IncorrectFormat @@ -995,7 +1004,7 @@ def remove_component_callback( except KeyError: raise error.IncorrectFormat( f"Callback for " - f"message ID {message_id or ''}, " + f"message ID `{message_id or ''}`, " f"custom_id `{custom_id or ''}`, " f"component_type `{component_type or ''}` is not registered!" ) @@ -1038,14 +1047,18 @@ def component_callback( .. note:: `message_id` and `message_ids` cannot be used at the same time. - :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. - :type component_type: Union[int, manage_components.ComponentsType] - :param custom_id: The `custom_id` of the component. Defaults to the name of coroutine being decorated. - :type custom_id: str :param message_id: If specified, only interactions from the message given will be accepted. :type message_id: Optional[int] - :param message_ids: Acts like `message_id`, but accepts a list of message IDs instead. + :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. :type message_ids: Optional[List[int]] + :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. + :type custom_id: Optional[str] + :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. + :type custom_ids: Optional[List[str]] + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. + :type use_callback_name: bool + :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :type component_type: Optional[int] :raises: .error.IncorrectFormat """ @@ -1167,9 +1180,9 @@ async def invoke_command(self, func, ctx, args): async def invoke_component_callback(self, func, ctx): """ - Invokes command. + Invokes component callback. - :param func: Component callback coroutine. + :param func: Component callback object. :param ctx: Context. """ try: diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index aaea41738..39989c574 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -18,7 +18,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: @@ -88,7 +88,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: @@ -173,6 +173,27 @@ def cog_component( use_callback_name=True, component_type: int = None, ): + """ + Decorator for component callbacks in cogs.\n + Almost same as :meth:`.client.SlashCommand.component_callback`. + + .. note:: + ``message_id`` and ``message_ids`` cannot be used at the same time. + + :param message_id: If specified, only interactions from the message given will be accepted. + :type message_id: Optional[int] + :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. + :type message_ids: Optional[List[int]] + :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. + :type custom_id: Optional[str] + :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. + :type custom_ids: Optional[List[str]] + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. + :type use_callback_name: bool + :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :type component_type: Optional[int] + :raises: .error.IncorrectFormat + """ if message_id and message_ids: raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") diff --git a/discord_slash/model.py b/discord_slash/model.py index eaf3cfe91..de3e74834 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -382,14 +382,14 @@ def __init__(self, base, cmd, sub_group, name, sub): class ComponentCallbackObject(CallbackObject): """ - Internal component object. + Internal component object. Inherits :class:`CallbackObject`, so it has all variables from it. .. warning:: Do not manually init this model. - :ivar custom_id: Custom ID of the component. - :ivar func: An optional single callback coroutine for the component. If the message ID given isn't in ``funcList``, this function will be ran instead. - :ivar funcList: An optional :class:`dict` with message IDs as keys and callback coroutines as values. If a message ID is found in the dict, the corresponding coroutine will be ran. + :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__( @@ -415,7 +415,7 @@ async def invoke(self, ctx): class CogComponentCallbackObject(ComponentCallbackObject): """ - Component callback object but for Cog. + Component callback object but for Cog. Has all variables from :class:`ComponentCallbackObject`. .. warning:: Do not manually init this model. From 695ce9dd5152b11acb781e574ea9a437e03e88ff Mon Sep 17 00:00:00 2001 From: Artem Date: Sun, 13 Jun 2021 23:39:39 +0300 Subject: [PATCH 08/15] Added ability to dynamically modify component callbacks --- discord_slash/client.py | 74 +++++++++++++++++++++------------------- discord_slash/cog_ext.py | 14 +++----- discord_slash/model.py | 15 +++++--- 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index d7d56fae0..eeab57fb4 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -903,32 +903,23 @@ def add_component_callback( :type message_id: Optional[int] :raises: .error.DuplicateCustomID """ - if message_id and message_ids: + if message_id and message_ids is not None: raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") - if custom_id and custom_ids: + if custom_id and custom_ids is not None: raise error.IncorrectFormat("You cannot use both `custom_id` and `custom_ids`!") - if message_id: + if message_ids is None: message_ids = [message_id] - if custom_id: - custom_ids = [custom_id] + if custom_ids is None: + custom_ids = [callback.__name__] if use_callback_name else [custom_id] - if use_callback_name: - custom_ids = custom_ids or [callback.__name__] - - if not (message_ids or custom_ids): + if message_ids == [None] and custom_ids == [None]: raise error.IncorrectFormat( "'message_ids' ('message_id') or 'custom_ids' ('custom_id') must be specified!" ) - if custom_id: - custom_ids = [custom_id] - - if use_callback_name: - custom_ids = custom_ids or [callback.__name__] - callback_obj = model.ComponentCallbackObject( callback, message_ids, custom_ids, component_type ) @@ -938,22 +929,31 @@ def add_component_callback( def _add_comp_callback_obj(self, callback_obj): component_type = callback_obj.component_type - for message_id in callback_obj.message_ids: - for custom_id in callback_obj.custom_ids: - message_id_dict = self.components - custom_id_dict = message_id_dict.setdefault(message_id, {}) - component_type_dict = custom_id_dict.setdefault(custom_id, {}) + for message_id, custom_id in callback_obj.keys: + self._register_comp_callback_obj(callback_obj, message_id, custom_id, component_type) - if component_type in component_type_dict: - raise error.DuplicateCallback(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, {}) - 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 ''}`" - ) + 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: model.ComponentCallbackObject, + message_id: int = None, custom_id: str = None): + + component_type = callback.component_type + self._register_comp_callback_obj(callback, message_id, custom_id, component_type) + callback.keys.add((message_id, custom_id)) def get_component_callback( self, @@ -976,7 +976,8 @@ def remove_component_callback( self, message_id: int = None, custom_id: str = None, component_type: int = None ): """ - Removes a component callback. If the `message_id` is specified, only removes the callback for the specific message ID. + Removes a component callback from specific combination of message_id, custom_id, component_type. + If the `message_id` is specified, only removes the callback for the specific message ID. :param custom_id: The `custom_id` of the component. :type custom_id: str @@ -1000,21 +1001,22 @@ def remove_component_callback( f"component_type `{component_type or ''}` is not registered!" ) else: - callback.message_ids.remove(message_id) - callback.custom_ids.remove(custom_id) + callback.keys.remove((message_id, custom_id)) def remove_component_callback_obj(self, callback_obj: model.ComponentCallbackObject): """ - Removes a component callback. + 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 in callback_obj.message_ids: - for custom_id in callback_obj.custom_ids: - self.remove_component_callback(message_id, custom_id, 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, diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index aaea41738..1343d7a19 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -179,22 +179,16 @@ def cog_component( if custom_id and custom_ids: raise error.IncorrectFormat("You cannot use both `custom_id` and `custom_ids`!") - if message_id: + if message_ids is None: message_ids = [message_id] - if custom_id: - custom_ids = [custom_id] - - if component_type not in (2, 3, None): - raise error.IncorrectFormat(f"Invalid component type `{component_type}`") - def wrapper(callback): nonlocal custom_ids - if use_callback_name: - custom_ids = custom_ids or [callback.__name__] + if custom_ids is None: + custom_ids = [callback.__name__] if use_callback_name else [custom_id] - if not (message_ids or custom_ids): + if message_ids == [None] and custom_ids == [None]: raise error.IncorrectFormat( "'message_ids' ('message_id') or 'custom_ids' ('custom_id') must be specified!" ) diff --git a/discord_slash/model.py b/discord_slash/model.py index eaf3cfe91..845bc21b6 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -395,13 +395,18 @@ class ComponentCallbackObject(CallbackObject): def __init__( self, func, - message_ids=None, - custom_ids=None, - component_type=None, + 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) - self.message_ids = message_ids or [None] - self.custom_ids = custom_ids or [None] + 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): From b5af3078bc19b941a3ced1eeaa2d9083ff1f071c Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 14 Jun 2021 00:38:54 +0300 Subject: [PATCH 09/15] Updated docs and formatting --- discord_slash/client.py | 73 +++++++++++++++++++++++++++------------- discord_slash/cog_ext.py | 11 +++--- discord_slash/model.py | 6 ++-- docs/events.rst | 17 ++++++++++ 4 files changed, 76 insertions(+), 31 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 7aefe0bfd..e099808e2 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -891,7 +891,9 @@ def add_component_callback( component_type: int = None, ): """ - Adds a coroutine callback to a component. Optionally, this can be made to only accept component interactions from a specific message. + 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. .. note:: ``message_id`` and ``message_ids`` cannot be used at the same time. The same applies to ``custom_id`` and ``custom_ids``. @@ -900,17 +902,18 @@ def add_component_callback( :type callback: Coroutine :param message_id: If specified, only interactions from the message given will be accepted. :type message_id: Optional[int] - :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. + :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. Empty list will mean that no interactions are accepted. :type message_ids: Optional[List[int]] :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. :type custom_id: Optional[str] - :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. + :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. :type custom_ids: Optional[List[str]] :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. + If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. :type use_callback_name: bool - :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :param component_type: The type of the component to avoid collisions with other component types. See :class:`.utils.manage_components.ComponentsType`. :type component_type: Optional[int] - :raises: .error.DuplicateCustomID + :raises: .error.DuplicateCustomID, .error.IncorrectFormat """ if message_id and message_ids is not None: raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") @@ -957,12 +960,25 @@ def _register_comp_callback_obj(self, callback_obj, message_id, custom_id, compo f"component_type `{component_type or ''}`" ) - def extend_component_callback(self, callback: model.ComponentCallbackObject, - message_id: int = None, custom_id: str = None): + def extend_component_callback( + self, callback_obj: model.ComponentCallbackObject, message_id: int = None, custom_id: str = None + ): + """ + Registers existing callback object (:class:`.utils.manage_components.ComponentsType`) + 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.component_type - self._register_comp_callback_obj(callback, message_id, custom_id, component_type) - callback.keys.add((message_id, custom_id)) + 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, @@ -970,6 +986,18 @@ def get_component_callback( 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:`.utils.manage_components.ComponentsType`. + :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) @@ -986,14 +1014,13 @@ def remove_component_callback( ): """ Removes a component callback from specific combination of message_id, custom_id, component_type. - If the `message_id` is specified, only removes the callback for the specific message ID. + :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:`.utils.manage_components.ComponentsType`. :type component_type: Optional[int] - :param message_id: If specified, only removes the callback for the specific message ID. - :type message_id: Optional[int] :raises: .error.IncorrectFormat """ try: @@ -1039,29 +1066,27 @@ def component_callback( ): """ Decorator that registers a coroutine as a component callback.\n - The second argument is the `custom_id` to listen for. - It will default to the coroutine name if unspecified.\n - The `message_id` keyword-only arg is optional, - but will make the callback only work with a specific message if given.\n - Alternatively, if it needs to accept interactions from multiple specific messages, the `message_ids` arg - accepts a list of message IDs. + 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. .. note:: - `message_id` and `message_ids` cannot be used at the same time. + ``message_id`` and ``message_ids`` cannot be used at the same time. The same applies to ``custom_id`` and ``custom_ids``. :param message_id: If specified, only interactions from the message given will be accepted. :type message_id: Optional[int] - :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. + :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. Empty list will mean that no interactions are accepted. :type message_ids: Optional[List[int]] :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. :type custom_id: Optional[str] - :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. + :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. :type custom_ids: Optional[List[str]] :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. + If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. :type use_callback_name: bool - :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :param component_type: The type of the component to avoid collisions with other component types. See :class:`.utils.manage_components.ComponentsType`. :type component_type: Optional[int] - :raises: .error.IncorrectFormat + :raises: .error.DuplicateCustomID, .error.IncorrectFormat """ def wrapper(callback): diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index 6d199e1d6..d15c25bf1 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -178,21 +178,22 @@ def cog_component( Almost same as :meth:`.client.SlashCommand.component_callback`. .. note:: - ``message_id`` and ``message_ids`` cannot be used at the same time. + ``message_id`` and ``message_ids`` cannot be used at the same time. The same applies to ``custom_id`` and ``custom_ids``. :param message_id: If specified, only interactions from the message given will be accepted. :type message_id: Optional[int] - :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. + :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. Empty list will mean that no interactions are accepted. :type message_ids: Optional[List[int]] :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. :type custom_id: Optional[str] - :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. + :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. :type custom_ids: Optional[List[str]] :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. + If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. :type use_callback_name: bool - :param component_type: The type of the component. See :class:`.utils.manage_components.ComponentsType`. + :param component_type: The type of the component to avoid collisions with other component types. See :class:`.utils.manage_components.ComponentsType`. :type component_type: Optional[int] - :raises: .error.IncorrectFormat + :raises: .error.DuplicateCustomID, .error.IncorrectFormat """ if message_id and message_ids: raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") diff --git a/discord_slash/model.py b/discord_slash/model.py index 67bb548ba..44c8489ef 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 @@ -405,7 +405,9 @@ def __init__( 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.keys = { + (message_id, custom_id) for message_id in message_ids for custom_id in custom_ids + } self.component_type = component_type 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 From dbcee7086194e90c3a937a3f264788971af9e056 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 14 Jun 2021 00:40:45 +0300 Subject: [PATCH 10/15] Updated formatting --- discord_slash/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index e099808e2..7775872a5 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -961,7 +961,10 @@ def _register_comp_callback_obj(self, callback_obj, message_id, custom_id, compo ) def extend_component_callback( - self, callback_obj: model.ComponentCallbackObject, message_id: int = None, custom_id: str = None + self, + callback_obj: model.ComponentCallbackObject, + message_id: int = None, + custom_id: str = None, ): """ Registers existing callback object (:class:`.utils.manage_components.ComponentsType`) From 9c7db190d471156c61c5e2e0e4e8d5aa337f8e66 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 14 Jun 2021 01:01:33 +0300 Subject: [PATCH 11/15] Updated in accordance to breaking changes introduced in #204 --- discord_slash/client.py | 10 +++++----- discord_slash/cog_ext.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 7775872a5..8d69e04e9 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -911,7 +911,7 @@ def add_component_callback( :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) 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:`.utils.manage_components.ComponentsType`. + :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 """ @@ -967,7 +967,7 @@ def extend_component_callback( custom_id: str = None, ): """ - Registers existing callback object (:class:`.utils.manage_components.ComponentsType`) + Registers existing callback object (:class:`.model.ComponentType`) for specific combination of message_id, custom_id, component_type. :param callback_obj: callback object. @@ -996,7 +996,7 @@ def get_component_callback( :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:`.utils.manage_components.ComponentsType`. + :param component_type: The type of the component. See :class:`.model.ComponentType`. :type component_type: Optional[int] :return: Optional[model.ComponentCallbackObject] @@ -1022,7 +1022,7 @@ def remove_component_callback( :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:`.utils.manage_components.ComponentsType`. + :param component_type: The type of the component. See :class:`.model.ComponentType`. :type component_type: Optional[int] :raises: .error.IncorrectFormat """ @@ -1087,7 +1087,7 @@ def component_callback( :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) 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:`.utils.manage_components.ComponentsType`. + :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 """ diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index d15c25bf1..bc5e1a37a 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -191,7 +191,7 @@ def cog_component( :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) 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:`.utils.manage_components.ComponentsType`. + :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 """ From 187b71b6af211435c6d885a4bd91ce0f20084578 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 14 Jun 2021 01:45:54 +0300 Subject: [PATCH 12/15] Fix sphinx errors --- discord_slash/client.py | 6 ++---- discord_slash/cog_ext.py | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 8d69e04e9..e301a7e92 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -908,8 +908,7 @@ def add_component_callback( :type custom_id: Optional[str] :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. :type custom_ids: Optional[List[str]] - :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. - If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) 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] @@ -1084,8 +1083,7 @@ def component_callback( :type custom_id: Optional[str] :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. :type custom_ids: Optional[List[str]] - :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. - If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) 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] diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index bc5e1a37a..9ab43624e 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -188,8 +188,7 @@ def cog_component( :type custom_id: Optional[str] :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. :type custom_ids: Optional[List[str]] - :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. - If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. + :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) 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] From 2e52f2bbe2aea1e6192daf9890db8bf9a720ac6e Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 15 Jun 2021 00:08:30 +0300 Subject: [PATCH 13/15] Changed component adding functions to accept lists (and objects) of custom_ids/message ids in the same arguments --- discord_slash/client.py | 70 ++++++++---------------- discord_slash/cog_ext.py | 44 +++++---------- discord_slash/utils/manage_components.py | 38 ++++++++----- 3 files changed, 60 insertions(+), 92 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index e301a7e92..2ab02a0bc 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -9,6 +9,7 @@ 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 @@ -883,10 +884,8 @@ def add_component_callback( self, callback: typing.Coroutine, *, - message_id: int = None, - message_ids: typing.List[int] = None, - custom_id: str = None, - custom_ids: typing.List[str] = None, + messages: typing.Union[int, discord.Message, list] = None, + components: typing.Union[str, dict, list] = None, use_callback_name=True, component_type: int = None, ): @@ -895,41 +894,27 @@ def add_component_callback( Callback can be made to only accept component interactions from a specific messages and/or custom_ids of components. - .. note:: - ``message_id`` and ``message_ids`` cannot be used at the same time. The same applies to ``custom_id`` and ``custom_ids``. - :param callback: The coroutine to be called when the component is interacted with. Must accept a single argument with the type :class:`.context.ComponentContext`. :type callback: Coroutine - :param message_id: If specified, only interactions from the message given will be accepted. - :type message_id: Optional[int] - :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. Empty list will mean that no interactions are accepted. - :type message_ids: Optional[List[int]] - :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. - :type custom_id: Optional[str] - :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. - :type custom_ids: Optional[List[str]] - :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. + :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 """ - if message_id and message_ids is not None: - raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") - if custom_id and custom_ids is not None: - raise error.IncorrectFormat("You cannot use both `custom_id` and `custom_ids`!") + 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 message_ids is None: - message_ids = [message_id] - - if custom_ids is None: - custom_ids = [callback.__name__] if use_callback_name else [custom_id] + if use_callback_name and custom_ids == [None]: + custom_ids = [callback.__name__] if message_ids == [None] and custom_ids == [None]: - raise error.IncorrectFormat( - "'message_ids' ('message_id') or 'custom_ids' ('custom_id') must be specified!" - ) + raise error.IncorrectFormat("You must specify messages or components (or both)") callback_obj = model.ComponentCallbackObject( callback, message_ids, custom_ids, component_type @@ -1059,10 +1044,8 @@ def remove_component_callback_obj(self, callback_obj: model.ComponentCallbackObj def component_callback( self, *, - message_id: int = None, - message_ids: typing.List[int] = None, - custom_id: str = None, - custom_ids: typing.List[str] = None, + messages: typing.Union[int, discord.Message, list] = None, + components: typing.Union[str, dict, list] = None, use_callback_name=True, component_type: int = None, ): @@ -1072,18 +1055,11 @@ def component_callback( Callback can be made to only accept component interactions from a specific messages and/or custom_ids of components. - .. note:: - ``message_id`` and ``message_ids`` cannot be used at the same time. The same applies to ``custom_id`` and ``custom_ids``. - - :param message_id: If specified, only interactions from the message given will be accepted. - :type message_id: Optional[int] - :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. Empty list will mean that no interactions are accepted. - :type message_ids: Optional[List[int]] - :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. - :type custom_id: Optional[str] - :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. - :type custom_ids: Optional[List[str]] - :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. + :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] @@ -1093,10 +1069,8 @@ def component_callback( def wrapper(callback): return self.add_component_callback( callback, - message_id=message_id, - message_ids=message_ids, - custom_id=custom_id, - custom_ids=custom_ids, + messages=messages, + components=components, use_callback_name=use_callback_name, component_type=component_type, ) diff --git a/discord_slash/cog_ext.py b/discord_slash/cog_ext.py index 9ab43624e..de37bb516 100644 --- a/discord_slash/cog_ext.py +++ b/discord_slash/cog_ext.py @@ -1,9 +1,12 @@ import inspect import typing +import discord + from . import error 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( @@ -166,10 +169,8 @@ def wrapper(cmd): def cog_component( *, - message_id: int = None, - message_ids: typing.List[int] = None, - custom_id: str = None, - custom_ids: typing.List[str] = None, + messages: typing.Union[int, discord.Message, list] = None, + components: typing.Union[str, dict, list] = None, use_callback_name=True, component_type: int = None, ): @@ -177,42 +178,27 @@ def cog_component( Decorator for component callbacks in cogs.\n Almost same as :meth:`.client.SlashCommand.component_callback`. - .. note:: - ``message_id`` and ``message_ids`` cannot be used at the same time. The same applies to ``custom_id`` and ``custom_ids``. - - :param message_id: If specified, only interactions from the message given will be accepted. - :type message_id: Optional[int] - :param message_ids: Similar to ``message_id``, but accepts a list of message IDs instead. Empty list will mean that no interactions are accepted. - :type message_ids: Optional[List[int]] - :param custom_id: The ``custom_id`` of the component. Defaults to the name of ``callback`` if ``use_callback_name=True``. - :type custom_id: Optional[str] - :param custom_ids: Similar to ``custom_ids``, but accepts a list of custom IDs instead. Empty list will mean that no interactions are accepted. - :type custom_ids: Optional[List[str]] - :param use_callback_name: Whether the ``custom_id`` defaults to the name of ``callback`` if unspecified. If ``False``, either `message_ids`` (``message_id``) or ``custom_ids`` (``custom_id``) must be specified. + :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 """ - if message_id and message_ids: - raise error.IncorrectFormat("You cannot use both `message_id` and `message_ids`!") - - if custom_id and custom_ids: - raise error.IncorrectFormat("You cannot use both `custom_id` and `custom_ids`!") - - if message_ids is None: - message_ids = [message_id] + 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 custom_ids is None: - custom_ids = [callback.__name__] if use_callback_name else [custom_id] + if use_callback_name and custom_ids == [None]: + custom_ids = [callback.__name__] if message_ids == [None] and custom_ids == [None]: - raise error.IncorrectFormat( - "'message_ids' ('message_id') or 'custom_ids' ('custom_id') must be specified!" - ) + raise error.IncorrectFormat("You must specify messages or components (or both)") return CogComponentCallbackObject( callback, diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 60f0bfbd1..7299fb459 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -152,9 +152,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 +175,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 +197,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 +208,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) From b27b4d093aa6ba426a48c8be1d368c070a066811 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 16 Jun 2021 22:28:15 +0300 Subject: [PATCH 14/15] Added function to spread list of components (buttons) over rows --- discord_slash/utils/manage_components.py | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 7299fb459..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. From 2ef0667d0a2a9c1da01d17a588a80a88df7f777a Mon Sep 17 00:00:00 2001 From: Artem Date: Fri, 18 Jun 2021 00:11:33 +0300 Subject: [PATCH 15/15] Adjusted docstrings and error message --- discord_slash/client.py | 5 ++--- discord_slash/error.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 2ab02a0bc..8d550acd9 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -894,8 +894,7 @@ def add_component_callback( Callback can be made to only accept component interactions from a specific messages and/or custom_ids of components. - :param callback: The coroutine to be called when the component is interacted with. Must accept a single argument with the type :class:`.context.ComponentContext`. - :type callback: Coroutine + :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. @@ -1050,7 +1049,7 @@ def component_callback( component_type: int = None, ): """ - Decorator that registers a coroutine as a component callback.\n + 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. diff --git a/discord_slash/error.py b/discord_slash/error.py index 8e55a457f..ed1ebc67f 100644 --- a/discord_slash/error.py +++ b/discord_slash/error.py @@ -42,12 +42,12 @@ def __init__(self, name: str): class DuplicateCallback(SlashCommandError): """ - There is a duplicate component custom ID. + There is a duplicate component callback. """ def __init__(self, message_id: int, custom_id: str, component_type: int): super().__init__( - f"Duplicate component detected: " + f"Duplicate component callback detected: " f"message ID {message_id or ''}, " f"custom_id `{custom_id or ''}`, " f"component_type `{component_type or ''}`"