From d4861babbb3ec9e085e251b3751cbb9196c2fba9 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 30 May 2021 15:47:00 +0300 Subject: [PATCH 01/32] Added components support --- discord_slash/client.py | 14 +- discord_slash/context.py | 46 +++- discord_slash/dpy_overrides.py | 254 +++++++++++++++++++++++ discord_slash/model.py | 7 +- discord_slash/utils/manage_components.py | 82 ++++++++ 5 files changed, 391 insertions(+), 12 deletions(-) create mode 100644 discord_slash/dpy_overrides.py create mode 100644 discord_slash/utils/manage_components.py diff --git a/discord_slash/client.py b/discord_slash/client.py index bb054d2ef..bd3adcc60 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -9,6 +9,7 @@ from . import model from . import error from . import context +from . import dpy_overrides from .utils import manage_commands @@ -886,10 +887,19 @@ async def on_socket_response(self, msg): return to_use = msg["d"] + interaction_type = to_use["type"] + if interaction_type in (1, 2): + return await self._on_slash(to_use) + if interaction_type == 3: + return await self._on_component(to_use) - if to_use["type"] not in (1, 2): - return # to only process ack and slash-commands and exclude other interactions like buttons + raise NotImplementedError + async def _on_component(self, to_use): + ctx = context.ComponentContext(self.req, to_use, self._discord, self.logger) + self._discord.dispatch("component", ctx) + + async def _on_slash(self, to_use): if to_use["data"]["name"] in self.commands: ctx = context.SlashContext(self.req, to_use, self._discord, self.logger) diff --git a/discord_slash/context.py b/discord_slash/context.py index 54dade599..df1b11e34 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -10,7 +10,7 @@ from . import model -class SlashContext: +class InteractionContext: """ Context of the slash command.\n Kinda similar with discord.ext.commands.Context. @@ -45,13 +45,7 @@ def __init__(self, logger): self.__token = _json["token"] self.message = None # Should be set later. - self.name = self.command = self.invoked_with = _json["data"]["name"] - self.args = [] - self.kwargs = {} - self.subcommand_name = self.invoked_subcommand = self.subcommand_passed = None - self.subcommand_group = self.invoked_subcommand_group = self.subcommand_group_passed = None self.interaction_id = _json["id"] - self.command_id = _json["data"]["id"] self._http = _http self.bot = _discord self._logger = logger @@ -130,7 +124,9 @@ async def send(self, files: typing.List[discord.File] = None, allowed_mentions: discord.AllowedMentions = None, hidden: bool = False, - delete_after: float = None) -> model.SlashMessage: + delete_after: float = None, + components: typing.List[dict] = None, + ) -> model.SlashMessage: """ Sends response of the slash command. @@ -157,6 +153,8 @@ async def send(self, :type hidden: bool :param delete_after: If provided, the number of seconds to wait in the background before deleting the message we just sent. If the deletion fails, then it is silently ignored. :type delete_after: float + :param components: Message components in the response. The top level must be made of ActionRows. + :type components: List[dict] :return: Union[discord.Message, dict] """ if embed and embeds: @@ -174,13 +172,16 @@ async def send(self, files = [file] if delete_after and hidden: raise error.IncorrectFormat("You can't delete a hidden message!") + if components and not all(comp.get("type") == 1 for comp in components): + raise error.IncorrectFormat("The top level of the components list must be made of ActionRows!") base = { "content": content, "tts": tts, "embeds": [x.to_dict() for x in embeds] if embeds else [], "allowed_mentions": allowed_mentions.to_dict() if allowed_mentions - else self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {} + else self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {}, + "components": components or [], } if hidden: base["flags"] = 64 @@ -227,3 +228,30 @@ async def send(self, return smsg else: return resp + + +class SlashContext(InteractionContext): + def __init__(self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger): + self.name = self.command = self.invoked_with = _json["data"]["name"] + self.args = [] + self.kwargs = {} + self.subcommand_name = self.invoked_subcommand = self.subcommand_passed = None + self.subcommand_group = self.invoked_subcommand_group = self.subcommand_group_passed = None + self.command_id = _json["data"]["id"] + + super().__init__(_http=_http, _json=_json, _discord=_discord, logger=logger) + + +class ComponentContext(InteractionContext): + def __init__(self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger): + self.custom_id = self.component_id = _json["data"]["custom_id"] + self.component_type = _json["data"]["component_type"] + super().__init__(_http=_http, _json=_json, _discord=_discord, logger=logger) diff --git a/discord_slash/dpy_overrides.py b/discord_slash/dpy_overrides.py new file mode 100644 index 000000000..58d63c732 --- /dev/null +++ b/discord_slash/dpy_overrides.py @@ -0,0 +1,254 @@ +import discord +from discord.ext import commands +from discord import AllowedMentions, InvalidArgument, File +from discord.http import Route +from discord import http +from discord import abc +from discord import utils + + +class ComponentMessage(discord.Message): + __slots__ = tuple(list(discord.Message.__slots__) + ["components"]) + + def __init__(self, *, state, channel, data): + super().__init__(state=state, channel=channel, data=data) + self.components = data['components'] + + +def new_override(cls, *args, **kwargs): + if cls is discord.Message: + return object.__new__(ComponentMessage) + else: + return object.__new__(cls) + + +discord.message.Message.__new__ = new_override + + +def send_files(self, channel_id, *, files, content=None, tts=False, embed=None, components=None, + nonce=None, allowed_mentions=None, message_reference=None): + r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) + form = [] + + payload = {'tts': tts} + if content: + payload['content'] = content + if embed: + payload['embed'] = embed + if components: + payload['components'] = components + if nonce: + payload['nonce'] = nonce + if allowed_mentions: + payload['allowed_mentions'] = allowed_mentions + if message_reference: + payload['message_reference'] = message_reference + + form.append({'name': 'payload_json', 'value': utils.to_json(payload)}) + if len(files) == 1: + file = files[0] + form.append({ + 'name': 'file', + 'value': file.fp, + 'filename': file.filename, + 'content_type': 'application/octet-stream' + }) + else: + for index, file in enumerate(files): + form.append({ + 'name': 'file%s' % index, + 'value': file.fp, + 'filename': file.filename, + 'content_type': 'application/octet-stream' + }) + + return self.request(r, form=form, files=files) + + +def send_message(self, channel_id, content, *, tts=False, embed=None, components=None, + nonce=None, allowed_mentions=None, message_reference=None): + r = Route('POST', '/channels/{channel_id}/messages', channel_id=channel_id) + payload = {} + + if content: + payload['content'] = content + + if tts: + payload['tts'] = True + + if embed: + payload['embed'] = embed + + if components: + payload['components'] = components + + if nonce: + payload['nonce'] = nonce + + if allowed_mentions: + payload['allowed_mentions'] = allowed_mentions + + if message_reference: + payload['message_reference'] = message_reference + + return self.request(r, json=payload) + + +http.HTTPClient.send_files = send_files +http.HTTPClient.send_message = send_message + + +async def send(self, content=None, *, tts=False, embed=None, file=None, components=None, + files=None, delete_after=None, nonce=None, + allowed_mentions=None, reference=None, + mention_author=None): + """|coro| + + Sends a message to the destination with the content given. + + The content must be a type that can convert to a string through ``str(content)``. + If the content is set to ``None`` (the default), then the ``embed`` parameter must + be provided. + + To upload a single file, the ``file`` parameter should be used with a + single :class:`~discord.File` object. To upload multiple files, the ``files`` + parameter should be used with a :class:`list` of :class:`~discord.File` objects. + **Specifying both parameters will lead to an exception**. + + If the ``embed`` parameter is provided, it must be of type :class:`~discord.Embed` and + it must be a rich embed type. + + Parameters + ------------ + content: :class:`str` + The content of the message to send. + tts: :class:`bool` + Indicates if the message should be sent using text-to-speech. + embed: :class:`~discord.Embed` + The rich embed for the content. + file: :class:`~discord.File` + The file to upload. + files: List[:class:`~discord.File`] + A list of files to upload. Must be a maximum of 10. + nonce: :class:`int` + The nonce to use for sending this message. If the message was successfully sent, + then the message will have a nonce with this value. + delete_after: :class:`float` + If provided, the number of seconds to wait in the background + before deleting the message we just sent. If the deletion fails, + then it is silently ignored. + allowed_mentions: :class:`~discord.AllowedMentions` + Controls the mentions being processed in this message. If this is + passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`. + The merging behaviour only overrides attributes that have been explicitly passed + to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`. + If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions` + are used instead. + + .. versionadded:: 1.4 + + reference: Union[:class:`~discord.Message`, :class:`~discord.MessageReference`] + A reference to the :class:`~discord.Message` to which you are replying, this can be created using + :meth:`~discord.Message.to_reference` or passed directly as a :class:`~discord.Message`. You can control + whether this mentions the author of the referenced message using the :attr:`~discord.AllowedMentions.replied_user` + attribute of ``allowed_mentions`` or by setting ``mention_author``. + + .. versionadded:: 1.6 + + mention_author: Optional[:class:`bool`] + If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``. + + .. versionadded:: 1.6 + + Raises + -------- + ~discord.HTTPException + Sending the message failed. + ~discord.Forbidden + You do not have the proper permissions to send the message. + ~discord.InvalidArgument + The ``files`` list is not of the appropriate size, + you specified both ``file`` and ``files``, + or the ``reference`` object is not a :class:`~discord.Message` + or :class:`~discord.MessageReference`. + + Returns + --------- + :class:`~discord.Message` + The message that was sent. + """ + + channel = await self._get_channel() + state = self._state + content = str(content) if content is not None else None + components = components or [] + if embed is not None: + embed = embed.to_dict() + + if allowed_mentions is not None: + if state.allowed_mentions is not None: + allowed_mentions = state.allowed_mentions.merge(allowed_mentions).to_dict() + else: + allowed_mentions = allowed_mentions.to_dict() + else: + allowed_mentions = state.allowed_mentions and state.allowed_mentions.to_dict() + + if mention_author is not None: + allowed_mentions = allowed_mentions or AllowedMentions().to_dict() + allowed_mentions['replied_user'] = bool(mention_author) + + if reference is not None: + try: + reference = reference.to_message_reference_dict() + except AttributeError: + raise InvalidArgument('reference parameter must be Message or MessageReference') from None + + if file is not None and files is not None: + raise InvalidArgument('cannot pass both file and files parameter to send()') + + if file is not None: + if not isinstance(file, File): + raise InvalidArgument('file parameter must be File') + + try: + data = await state.http.send_files(channel.id, files=[file], allowed_mentions=allowed_mentions, + content=content, tts=tts, embed=embed, nonce=nonce, + components=components, + message_reference=reference) + finally: + file.close() + + elif files is not None: + if len(files) > 10: + raise InvalidArgument('files parameter must be a list of up to 10 elements') + elif not all(isinstance(file, File) for file in files): + raise InvalidArgument('files parameter must be a list of File') + + try: + data = await state.http.send_files(channel.id, files=files, content=content, tts=tts, + embed=embed, nonce=nonce, allowed_mentions=allowed_mentions, + components=components, + message_reference=reference) + finally: + for f in files: + f.close() + else: + data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, components=components, + nonce=nonce, allowed_mentions=allowed_mentions, + message_reference=reference) + + ret = state.create_message(channel=channel, data=data) + if delete_after is not None: + await ret.delete(delay=delete_after) + return ret + + +async def send_override(context_or_channel, *args, **kwargs): + if isinstance(context_or_channel, commands.Context): + channel = context_or_channel.channel + else: + channel = context_or_channel + + return await send(channel, *args, **kwargs) + +abc.Messageable.send = send_override diff --git a/discord_slash/model.py b/discord_slash/model.py index 8cf738e33..2a1ddd0a9 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -5,6 +5,7 @@ from inspect import iscoroutinefunction from . import http from . import error +from . dpy_overrides import ComponentMessage class ChoiceData: @@ -365,7 +366,7 @@ def from_type(cls, t: type): if issubclass(t, discord.abc.Role): return cls.ROLE -class SlashMessage(discord.Message): +class SlashMessage(ComponentMessage): """discord.py's :class:`discord.Message` but overridden ``edit`` and ``delete`` to work for slash command.""" def __init__(self, *, state, channel, data, _http: http.SlashCommandRequest, interaction_token): @@ -388,6 +389,10 @@ async def _slash_edit(self, **fields): embeds = fields.get("embeds") file = fields.get("file") files = fields.get("files") + components = fields.get("components") + + if components: + _resp["components"] = components if embed and embeds: raise error.IncorrectFormat("You can't use both `embed` and `embeds`!") diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py new file mode 100644 index 000000000..9056c997c --- /dev/null +++ b/discord_slash/utils/manage_components.py @@ -0,0 +1,82 @@ +import uuid +import enum +import typing +import discord +from ..error import IncorrectFormat + + +class ComponentsType(enum.IntEnum): + actionrow = 1 + button = 2 + + +def create_actionrow(*components: dict) -> dict: + """ + Creates an ActionRow for message components. + :param components: Components to go within the ActionRow. + :return: dict + """ + + return { + "type": ComponentsType.actionrow, + "components": components + } + + +class ButtonStyle(enum.IntEnum): + blue = 1 + gray = 2 + grey = 2 + green = 3 + red = 4 + URL = 5 + + +def create_button(style: int, + label: str = None, + emoji: typing.Union[discord.Emoji, dict] = None, + custom_id: str = None, + url: str = None, + disabled: bool = False) -> dict: + if style == 5 and custom_id: + raise IncorrectFormat("A link button cannot have a `custom_id`!") + if style == 5 and not url: + raise IncorrectFormat("A link button must have a `url`!") + if url and style != 5: + raise IncorrectFormat("You can't have a URL on a non-link button!") + if not label and not emoji: + raise IncorrectFormat("You must have at least a label or emoji on a button.") + if not custom_id and style != 5: + custom_id = uuid.uuid4().int + + if isinstance(emoji, discord.Emoji): + emoji = {"name": emoji.name, "id": emoji.id, "animated": emoji.animated} + + return { + "type": ComponentsType.button, + "style": style, + "label": label if label else "", + "emoji": emoji if emoji else {}, + "custom_id": custom_id if custom_id else "", + "url": url if url else "", + "disabled": disabled + } + + +async def wait_for_component(client, component, check=None, timeout=None): + def _check(ctx): + if check and not check(ctx): + return False + return component["custom_id"] == ctx.custom_id + + return await client.wait_for("component", check=_check, timeout=timeout) + + +async def wait_for_any_component(client, message, check=None, timeout=None): + def _check(ctx): + if check and not check(ctx): + return False + return message.id == ctx.custom_id + + return await client.wait_for("component", check=_check, timeout=timeout) + From 4b21542254f0f47a2150cf907a35fdf9f038900e Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 30 May 2021 16:35:47 +0300 Subject: [PATCH 02/32] Added manage_components to init --- discord_slash/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index edffe6ebb..f493cbecf 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -12,5 +12,6 @@ from .model import SlashCommandOptionType from .context import SlashContext from .utils import manage_commands +from .utils import manage_components __version__ = "1.2.0" From 2df0fdfc9d1ec77176431744c3e03b04dd0fd202 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 30 May 2021 16:55:50 +0300 Subject: [PATCH 03/32] Added default emoji support to buttons --- discord_slash/utils/manage_components.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 9056c997c..b8e3cb900 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -51,6 +51,8 @@ def create_button(style: int, if isinstance(emoji, discord.Emoji): emoji = {"name": emoji.name, "id": emoji.id, "animated": emoji.animated} + elif isinstance(emoji, str): + emoji = {"name": emoji, "id": None} return { "type": ComponentsType.button, From 3f4c9b465514431665c5e449f2e86518dd90d20c Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 30 May 2021 16:56:00 +0300 Subject: [PATCH 04/32] Bump version --- discord_slash/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index f493cbecf..c23ad6617 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -14,4 +14,4 @@ from .utils import manage_commands from .utils import manage_components -__version__ = "1.2.0" +__version__ = "1.2.1" From 0946f892b1079c1f54310033439994b75dfd165d Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 30 May 2021 19:12:58 +0300 Subject: [PATCH 05/32] Fixed wait_for_any_component, added method to edit component message as part of interaction --- discord_slash/__init__.py | 2 + discord_slash/context.py | 91 ++++++++++++++++++++++-- discord_slash/utils/manage_components.py | 2 +- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index c23ad6617..301c592f5 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -11,6 +11,8 @@ from .client import SlashCommand from .model import SlashCommandOptionType from .context import SlashContext +from .context import ComponentContext +from .dpy_overrides import ComponentMessage from .utils import manage_commands from .utils import manage_components diff --git a/discord_slash/context.py b/discord_slash/context.py index df1b11e34..ab4e0fd39 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -8,6 +8,7 @@ from . import http from . import error from . import model +from . dpy_overrides import ComponentMessage class InteractionContext: @@ -43,7 +44,7 @@ def __init__(self, _json: dict, _discord: typing.Union[discord.Client, commands.Bot], logger): - self.__token = _json["token"] + self._token = _json["token"] self.message = None # Should be set later. self.interaction_id = _json["id"] self._http = _http @@ -112,7 +113,7 @@ async def defer(self, hidden: bool = False): if hidden: base["data"] = {"flags": 64} self._deferred_hidden = True - await self._http.post_initial_response(base, self.interaction_id, self.__token) + await self._http.post_initial_response(base, self.interaction_id, self._token) self.deferred = True async def send(self, @@ -197,21 +198,21 @@ async def send(self, "Deferred response might not be what you set it to! (hidden / visible) " "This is because it was deferred in a different state." ) - resp = await self._http.edit(base, self.__token, files=files) + resp = await self._http.edit(base, self._token, files=files) self.deferred = False else: json_data = { "type": 4, "data": base } - await self._http.post_initial_response(json_data, self.interaction_id, self.__token) + await self._http.post_initial_response(json_data, self.interaction_id, self._token) if not hidden: - resp = await self._http.edit({}, self.__token) + resp = await self._http.edit({}, self._token) else: resp = {} self.responded = True else: - resp = await self._http.post_followup(base, self.__token, files=files) + resp = await self._http.post_followup(base, self._token, files=files) if files: for file in files: file.close() @@ -220,7 +221,7 @@ async def send(self, data=resp, channel=self.channel or discord.Object(id=self.channel_id), _http=self._http, - interaction_token=self.__token) + interaction_token=self._token) if delete_after: self.bot.loop.create_task(smsg.delete(delay=delete_after)) if initial_message: @@ -255,3 +256,79 @@ def __init__(self, self.custom_id = self.component_id = _json["data"]["custom_id"] self.component_type = _json["data"]["component_type"] super().__init__(_http=_http, _json=_json, _discord=_discord, logger=logger) + self.origin_message = None + self.origin_message_id = int(_json["message"]["id"]) if "message" in _json.keys() else None + + if self.origin_message_id: + self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, + data=_json["message"]) + + async def defer(self, hidden: bool = False, edit_origin = False): + """ + 'Defers' the response, showing a loading state to the user + + :param hidden: Whether the deferred response should be ephemeral . Default ``False``. + """ + if self.deferred or self.responded: + raise error.AlreadyResponded("You have already responded to this command!") + base = {"type": 6 if edit_origin else 5} + if hidden and not edit_origin: + base["data"] = {"flags": 64} + self._deferred_hidden = True + await self._http.post_initial_response(base, self.interaction_id, self._token) + self.deferred = True + + async def edit_origin(self, **fields) -> model.SlashMessage: + _resp = {} + + content = fields.get("content") + if content: + _resp["content"] = str(content) + + embed = fields.get("embed") + embeds = fields.get("embeds") + file = fields.get("file") + files = fields.get("files") + components = fields.get("components") + + if components: + _resp["components"] = components + + if embed and embeds: + raise error.IncorrectFormat("You can't use both `embed` and `embeds`!") + if file and files: + raise error.IncorrectFormat("You can't use both `file` and `files`!") + if file: + files = [file] + if embed: + embeds = [embed] + if embeds: + if not isinstance(embeds, list): + raise error.IncorrectFormat("Provide a list of embeds.") + elif len(embeds) > 10: + raise error.IncorrectFormat("Do not provide more than 10 embeds.") + _resp["embeds"] = [x.to_dict() for x in embeds] + + allowed_mentions = fields.get("allowed_mentions") + _resp["allowed_mentions"] = allowed_mentions.to_dict() if allowed_mentions else \ + self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {} + + if not self.responded: + if files and not self.deferred: + await self.defer(edit_origin=True) + if self.deferred: + await self._http.edit(_resp, self._token, files=files) + self.deferred = False + else: + json_data = { + "type": 7, + "data": _resp + } + await self._http.post_initial_response(json_data, self.interaction_id, self._token) + self.responded = True + else: + raise error.IncorrectFormat("Already responded") + + if files: + for file in files: + file.close() diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index b8e3cb900..73ae5baf8 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -78,7 +78,7 @@ async def wait_for_any_component(client, message, check=None, timeout=None): def _check(ctx): if check and not check(ctx): return False - return message.id == ctx.custom_id + return message.id == ctx.origin_message_id return await client.wait_for("component", check=_check, timeout=timeout) From c451cff742b979357ca6191ad29d9b86f80312c9 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 30 May 2021 21:04:54 +0300 Subject: [PATCH 06/32] Creating unspecified custom_id for buttons as str of uuid --- discord_slash/utils/manage_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 73ae5baf8..2e7cbd26e 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -47,7 +47,7 @@ def create_button(style: int, if not label and not emoji: raise IncorrectFormat("You must have at least a label or emoji on a button.") if not custom_id and style != 5: - custom_id = uuid.uuid4().int + custom_id = str(uuid.uuid4()) if isinstance(emoji, discord.Emoji): emoji = {"name": emoji.name, "id": emoji.id, "animated": emoji.animated} From 5f98425617d9278f564d38ef4570f1f533893344 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Mon, 31 May 2021 23:10:29 +0300 Subject: [PATCH 07/32] Added select and select-option generation functions --- discord_slash/utils/manage_components.py | 44 ++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 2e7cbd26e..8ea88bf8c 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -8,6 +8,7 @@ class ComponentsType(enum.IntEnum): actionrow = 1 button = 2 + select = 3 def create_actionrow(*components: dict) -> dict: @@ -32,6 +33,14 @@ class ButtonStyle(enum.IntEnum): URL = 5 +def emoji_to_dict(emoji): + if isinstance(emoji, discord.Emoji): + emoji = {"name": emoji.name, "id": emoji.id, "animated": emoji.animated} + elif isinstance(emoji, str): + emoji = {"name": emoji, "id": None} + return emoji if emoji else {} + + def create_button(style: int, label: str = None, emoji: typing.Union[discord.Emoji, dict] = None, @@ -49,22 +58,45 @@ def create_button(style: int, if not custom_id and style != 5: custom_id = str(uuid.uuid4()) - if isinstance(emoji, discord.Emoji): - emoji = {"name": emoji.name, "id": emoji.id, "animated": emoji.animated} - elif isinstance(emoji, str): - emoji = {"name": emoji, "id": None} + emoji = emoji_to_dict(emoji) return { "type": ComponentsType.button, "style": style, "label": label if label else "", - "emoji": emoji if emoji else {}, - "custom_id": custom_id if custom_id else "", + "emoji": emoji, + "custom_id": custom_id, "url": url if url else "", "disabled": disabled } +def create_select_option(label: str, value: str, emoji=None, description: str = None, default=False): + emoji = emoji_to_dict(emoji) + + return { + "label": label, + "value": value, + "description": description, + "default": default, + "emoji": emoji + } + + +def create_select(options: list[dict], custom_id=None, placeholder=None, min_values=None, max_values=None): + if not len(options) or len(options) > 25: + raise IncorrectFormat("Options length should be between 1 and 25.") + + return { + "type": ComponentsType.select, + "options": options, + "custom_id": custom_id or str(uuid.uuid4()), + "placeholder": placeholder or "", + "min_values": min_values, + "max_values": max_values, + } + + async def wait_for_component(client, component, check=None, timeout=None): def _check(ctx): if check and not check(ctx): From a78379190491a4222464219e9c89d4b8fe4e1fd6 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Tue, 1 Jun 2021 16:19:52 +0300 Subject: [PATCH 08/32] Updated button generation code --- discord_slash/utils/manage_components.py | 52 ++++++++++++++++-------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 8ea88bf8c..bd9bb8417 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -2,6 +2,7 @@ import enum import typing import discord +from ..context import ComponentContext from ..error import IncorrectFormat @@ -17,6 +18,10 @@ def create_actionrow(*components: dict) -> dict: :param components: Components to go within the ActionRow. :return: dict """ + if not components or len(components) > 5: + raise IncorrectFormat("Number of components in one row should be between 1 and 25.") + if ComponentsType.select in [component["type"] for component in components] and len(components) > 1: + raise IncorrectFormat("Action row must have only one select component and nothing else") return { "type": ComponentsType.actionrow, @@ -26,12 +31,18 @@ def create_actionrow(*components: dict) -> dict: class ButtonStyle(enum.IntEnum): blue = 1 + blurple = 1 gray = 2 grey = 2 green = 3 red = 4 URL = 5 + primary = 1 + secondary = 2 + success = 3 + danger = 4 + def emoji_to_dict(emoji): if isinstance(emoji, discord.Emoji): @@ -41,35 +52,44 @@ def emoji_to_dict(emoji): return emoji if emoji else {} -def create_button(style: int, +def create_button(style: ButtonStyle, label: str = None, emoji: typing.Union[discord.Emoji, dict] = None, custom_id: str = None, url: str = None, disabled: bool = False) -> dict: - if style == 5 and custom_id: - raise IncorrectFormat("A link button cannot have a `custom_id`!") - if style == 5 and not url: - raise IncorrectFormat("A link button must have a `url`!") - if url and style != 5: + if style == ButtonStyle.URL: + if custom_id: + raise IncorrectFormat("A link button cannot have a `custom_id`!") + if not url: + raise IncorrectFormat("A link button must have a `url`!") + elif url: raise IncorrectFormat("You can't have a URL on a non-link button!") + if not label and not emoji: raise IncorrectFormat("You must have at least a label or emoji on a button.") - if not custom_id and style != 5: - custom_id = str(uuid.uuid4()) emoji = emoji_to_dict(emoji) - return { + data = { "type": ComponentsType.button, "style": style, - "label": label if label else "", - "emoji": emoji, - "custom_id": custom_id, - "url": url if url else "", - "disabled": disabled } + if label: + data["label"] = label + if emoji: + data["emoji"] = emoji + if disabled: + data["disabled"] = disabled + + if style == ButtonStyle.URL: + data["url"] = url + else: + data["custom_id"] = custom_id or str(uuid.uuid4()) + + return data + def create_select_option(label: str, value: str, emoji=None, description: str = None, default=False): emoji = emoji_to_dict(emoji) @@ -97,7 +117,7 @@ def create_select(options: list[dict], custom_id=None, placeholder=None, min_val } -async def wait_for_component(client, component, check=None, timeout=None): +async def wait_for_component(client, component, check=None, timeout=None) -> ComponentContext: def _check(ctx): if check and not check(ctx): return False @@ -106,7 +126,7 @@ def _check(ctx): return await client.wait_for("component", check=_check, timeout=timeout) -async def wait_for_any_component(client, message, check=None, timeout=None): +async def wait_for_any_component(client, message, check=None, timeout=None) -> ComponentContext: def _check(ctx): if check and not check(ctx): return False From a6e8ac4d09d57c8254865e493a3aaa02b6f5ac59 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Tue, 1 Jun 2021 16:20:35 +0300 Subject: [PATCH 09/32] Fixed processing component context from ephemeral message --- discord_slash/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord_slash/context.py b/discord_slash/context.py index ab4e0fd39..aa2589eed 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -259,7 +259,7 @@ def __init__(self, self.origin_message = None self.origin_message_id = int(_json["message"]["id"]) if "message" in _json.keys() else None - if self.origin_message_id: + if self.origin_message_id and (_json["message"]["flags"] & 64) != 64: self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, data=_json["message"]) From 5dbaee7a6f7ea1f9bcf6cb1db307410ac89cf67e Mon Sep 17 00:00:00 2001 From: hpenney2 Date: Tue, 1 Jun 2021 15:11:01 -0500 Subject: [PATCH 10/32] Add/edit docs for new stuff + code edits --- discord_slash/context.py | 40 ++++++++--- discord_slash/utils/manage_components.py | 70 +++++++++++++++++-- .../discord_slash.utils.manage_components.rst | 7 ++ docs/discord_slash.utils.rst | 1 + docs/events.rst | 7 ++ 5 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 docs/discord_slash.utils.manage_components.rst diff --git a/discord_slash/context.py b/discord_slash/context.py index aa2589eed..580e9602a 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -13,20 +13,14 @@ class InteractionContext: """ - Context of the slash command.\n + Base context for interactions.\n Kinda similar with discord.ext.commands.Context. .. warning:: Do not manually init this model. :ivar message: Message that invoked the slash command. - :ivar name: Name of the command. - :ivar args: List of processed arguments invoked with the command. - :ivar kwargs: Dictionary of processed arguments invoked with the command. - :ivar subcommand_name: Subcommand of the command. - :ivar subcommand_group: Subcommand group of the command. :ivar interaction_id: Interaction ID of the command message. - :ivar command_id: ID of the command. :ivar bot: discord.py client. :ivar _http: :class:`.http.SlashCommandRequest` of the client. :ivar _logger: Logger instance. @@ -232,6 +226,16 @@ async def send(self, class SlashContext(InteractionContext): + """ + Context of a slash command. Has all variables from :class:`InteractionContext`, plus the slash-command-specific ones below. + + :ivar name: Name of the command. + :ivar args: List of processed arguments invoked with the command. + :ivar kwargs: Dictionary of processed arguments invoked with the command. + :ivar subcommand_name: Subcommand of the command. + :ivar subcommand_group: Subcommand group of the command. + :ivar command_id: ID of the command. + """ def __init__(self, _http: http.SlashCommandRequest, _json: dict, @@ -248,6 +252,14 @@ def __init__(self, class ComponentContext(InteractionContext): + """ + Context of a component interaction. Has all variables from :class:`InteractionContext`, plus the component-specific ones below. + + :ivar custom_id: The custom ID of the component. + :ivar component_type: The type of the component. + :ivar origin_message: The origin message of the component. Not available if the origin message was ephemeral. + :ivar origin_message_id: The ID of the origin message. Not available if the origin message was ephemeral. + """ def __init__(self, _http: http.SlashCommandRequest, _json: dict, @@ -268,6 +280,7 @@ async def defer(self, hidden: bool = False, edit_origin = False): 'Defers' the response, showing a loading state to the user :param hidden: Whether the deferred response should be ephemeral . Default ``False``. + :param edit_origin: Whether the response is editting the origin message. If ``False``, the deferred response will be for a follow up message. Defaults ``False``. """ if self.deferred or self.responded: raise error.AlreadyResponded("You have already responded to this command!") @@ -278,7 +291,11 @@ async def defer(self, hidden: bool = False, edit_origin = False): await self._http.post_initial_response(base, self.interaction_id, self._token) self.deferred = True - async def edit_origin(self, **fields) -> model.SlashMessage: + async def edit_origin(self, **fields): + """ + Edits the origin message of the component. + Refer to :meth:`discord.Message.edit` and :meth:`InteractionContext.send` for fields. + """ _resp = {} content = fields.get("content") @@ -317,14 +334,14 @@ async def edit_origin(self, **fields) -> model.SlashMessage: if files and not self.deferred: await self.defer(edit_origin=True) if self.deferred: - await self._http.edit(_resp, self._token, files=files) + _json = await self._http.edit(_resp, self._token, files=files) self.deferred = False else: json_data = { "type": 7, "data": _resp } - await self._http.post_initial_response(json_data, self.interaction_id, self._token) + _json = await self._http.post_initial_response(json_data, self.interaction_id, self._token) self.responded = True else: raise error.IncorrectFormat("Already responded") @@ -332,3 +349,6 @@ async def edit_origin(self, **fields) -> model.SlashMessage: if files: for file in files: file.close() + + self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, + data=_json["message"]) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index bd9bb8417..f5874acd5 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -15,11 +15,12 @@ class ComponentsType(enum.IntEnum): def create_actionrow(*components: dict) -> dict: """ Creates an ActionRow for message components. + :param components: Components to go within the ActionRow. :return: dict """ if not components or len(components) > 5: - raise IncorrectFormat("Number of components in one row should be between 1 and 25.") + raise IncorrectFormat("Number of components in one row should be between 1 and 5.") if ComponentsType.select in [component["type"] for component in components] and len(components) > 1: raise IncorrectFormat("Action row must have only one select component and nothing else") @@ -44,7 +45,13 @@ class ButtonStyle(enum.IntEnum): danger = 4 -def emoji_to_dict(emoji): +def emoji_to_dict(emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str]) -> dict: + """ + Converts a default or custom emoji into a partial emoji dict. + + :param emoji: The emoji to convert. + :type emoji: Union[discord.Emoji, discord.PartialEmoji, str] + """ if isinstance(emoji, discord.Emoji): emoji = {"name": emoji.name, "id": emoji.id, "animated": emoji.animated} elif isinstance(emoji, str): @@ -52,12 +59,32 @@ def emoji_to_dict(emoji): return emoji if emoji else {} -def create_button(style: ButtonStyle, +def create_button(style: typing.Union[ButtonStyle, int], label: str = None, - emoji: typing.Union[discord.Emoji, dict] = None, + emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str] = None, custom_id: str = None, url: str = None, disabled: bool = False) -> dict: + """ + Creates a button component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). + + .. note:: + At least a label or emoji is required for a button. You can have both, but not neither of them. + + :param style: Style of the button. Refer to :class:`ButtonStyle`. + :type style: Union[ButtonStyle, int] + :param label: The label of the button. + :type label: Optional[str] + :param emoji: The emoji of the button. + :type emoji: Union[discord.Emoji, discord.PartialEmoji, dict] + :param custom_id: The custom_id of the button. Needed for non-link buttons. + :type custom_id: Optional[str] + :param url: The URL of the button. Needed for link buttons. + :type url: Optional[str] + :param disabled: Whether the button is disabled or not. Defaults to `False`. + :type disabled: bool + :returns: :class:`dict` + """ if style == ButtonStyle.URL: if custom_id: raise IncorrectFormat("A link button cannot have a `custom_id`!") @@ -91,7 +118,16 @@ def create_button(style: ButtonStyle, return data -def create_select_option(label: str, value: str, emoji=None, description: str = None, default=False): +def create_select_option(label: str, value: str, emoji=None, description: str = None, default: bool = False): + """ + Creates an option for select components. + + :param label: The label of the option. + :param value: The value that the bot will recieve when this option is selected. + :param emoji: The emoji of the option. + :param description: A description of the option. + :param default: Whether or not this is the default option. + """ emoji = emoji_to_dict(emoji) return { @@ -104,6 +140,12 @@ def create_select_option(label: str, value: str, emoji=None, description: str = def create_select(options: list[dict], custom_id=None, placeholder=None, min_values=None, max_values=None): + """ + Creates a select (dropdown) component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). + + .. warning:: + Currently, select components are not available for public use, nor have official documentation. The parameters will not be documented at this time. + """ if not len(options) or len(options) > 25: raise IncorrectFormat("Options length should be between 1 and 25.") @@ -118,6 +160,15 @@ def create_select(options: list[dict], custom_id=None, placeholder=None, min_val async def wait_for_component(client, component, check=None, timeout=None) -> ComponentContext: + """ + Waits for a component interaction. Only accepts interactions based on the custom ID of the component, and optionally a check function. + + :param client: The client/bot object. + :param component: The component dict. + :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` + """ def _check(ctx): if check and not check(ctx): return False @@ -127,6 +178,15 @@ def _check(ctx): async def wait_for_any_component(client, message, check=None, timeout=None) -> ComponentContext: + """ + Waits for any component interaction. Only accepts interactions based on the message ID given and optionally a check function. + + :param client: The client/bot object. + :param message: The message object to check for. + :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` + """ def _check(ctx): if check and not check(ctx): return False diff --git a/docs/discord_slash.utils.manage_components.rst b/docs/discord_slash.utils.manage_components.rst new file mode 100644 index 000000000..b2103f5f4 --- /dev/null +++ b/docs/discord_slash.utils.manage_components.rst @@ -0,0 +1,7 @@ +discord\_slash.utils.manage\_components module +============================================== + +.. automodule:: discord_slash.utils.manage_components + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/discord_slash.utils.rst b/docs/discord_slash.utils.rst index b9abe14c6..dd460bd73 100644 --- a/docs/discord_slash.utils.rst +++ b/docs/discord_slash.utils.rst @@ -8,6 +8,7 @@ Submodules :maxdepth: 4 discord_slash.utils.manage_commands + discord_slash.utils.manage_components Module contents --------------- diff --git a/docs/events.rst b/docs/events.rst index d8cbf671f..f695fd5e4 100644 --- a/docs/events.rst +++ b/docs/events.rst @@ -20,3 +20,10 @@ These events can be registered to discord.py's listener or :param ex: Exception that raised. :type ex: Exception +.. function:: on_component(ctx) + + Called when a component is triggered. + + :param ctx: ComponentContext of the triggered component. + :type ctx: :class:`.model.ComponentContext` + From 87c9ee65ab277cc5d063d5f14629d8c9ce993382 Mon Sep 17 00:00:00 2001 From: hpenney2 Date: Tue, 1 Jun 2021 15:54:53 -0500 Subject: [PATCH 11/32] Better wait_for_component and wait_for_any_component parameters --- discord_slash/context.py | 5 +++-- discord_slash/utils/manage_components.py | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/discord_slash/context.py b/discord_slash/context.py index 580e9602a..a085444f8 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -350,5 +350,6 @@ async def edit_origin(self, **fields): for file in files: file.close() - self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, - data=_json["message"]) + # Commented out for now as sometimes (or at least, when not deferred) _json is an empty string? + # self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, + # data=_json) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index f5874acd5..94890ff2c 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -159,12 +159,15 @@ def create_select(options: list[dict], custom_id=None, placeholder=None, min_val } -async def wait_for_component(client, component, check=None, timeout=None) -> ComponentContext: +async def wait_for_component(client: discord.Client, component: typing.Union[dict, str], check=None, timeout=None) \ + -> ComponentContext: """ Waits for a component interaction. Only accepts interactions based on the custom ID of the component, and optionally a check function. :param client: The client/bot object. - :param component: The component dict. + :type client: :class:`discord.Client` + :param component: The component dict or custom ID. + :type component: Union[dict, str] :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` @@ -172,17 +175,20 @@ async def wait_for_component(client, component, check=None, timeout=None) -> Com def _check(ctx): if check and not check(ctx): return False - return component["custom_id"] == ctx.custom_id + return (component["custom_id"] if isinstance(component, dict) else component) == ctx.custom_id return await client.wait_for("component", check=_check, timeout=timeout) -async def wait_for_any_component(client, message, check=None, timeout=None) -> ComponentContext: +async def wait_for_any_component(client: discord.Client, message: typing.Union[discord.Message, int], + check=None, timeout=None) -> ComponentContext: """ Waits for any component interaction. Only accepts interactions based on the message ID given and optionally a check function. :param client: The client/bot object. - :param message: The message object to check for. + :type client: :class:`discord.Client` + :param message: The message object to check for, or the message ID. + :type message: Union[discord.Message, int] :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` @@ -190,7 +196,6 @@ async def wait_for_any_component(client, message, check=None, timeout=None) -> C def _check(ctx): if check and not check(ctx): return False - return message.id == ctx.origin_message_id + return (message.id if isinstance(message, discord.Message) else message) == ctx.origin_message_id return await client.wait_for("component", check=_check, timeout=timeout) - From 553d85bd4fb6f3d374f2eaff7d8c46b21247090b Mon Sep 17 00:00:00 2001 From: hpenney2 Date: Tue, 1 Jun 2021 16:11:13 -0500 Subject: [PATCH 12/32] Fix docs for origin_message_id --- discord_slash/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord_slash/context.py b/discord_slash/context.py index a085444f8..a2f80fc99 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -258,7 +258,7 @@ class ComponentContext(InteractionContext): :ivar custom_id: The custom ID of the component. :ivar component_type: The type of the component. :ivar origin_message: The origin message of the component. Not available if the origin message was ephemeral. - :ivar origin_message_id: The ID of the origin message. Not available if the origin message was ephemeral. + :ivar origin_message_id: The ID of the origin message. """ def __init__(self, _http: http.SlashCommandRequest, From 1a1f85be5881ac8d789d560dbbdc90fdb07225e1 Mon Sep 17 00:00:00 2001 From: LordOfPolls Date: Wed, 2 Jun 2021 08:18:00 +0100 Subject: [PATCH 13/32] Add cooldown and max conc support ++ Fixes kwarg issue in invoke --- discord_slash/client.py | 5 +- discord_slash/context.py | 4 + discord_slash/model.py | 186 +++++++++++++++++++++------------------ 3 files changed, 107 insertions(+), 88 deletions(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index bd3adcc60..0ff6debc7 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -870,7 +870,10 @@ async def invoke_command(self, func, ctx, args): :param args: Args. Can be list or dict. """ try: - await func.invoke(ctx, args) + if isinstance(args, dict): + await func.invoke(ctx, **args) + else: + await func.invoke(ctx, *args) except Exception as ex: await self.on_slash_command_error(ctx, ex) diff --git a/discord_slash/context.py b/discord_slash/context.py index a2f80fc99..af7dea125 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -1,3 +1,4 @@ +import datetime import typing import asyncio from warnings import warn @@ -5,6 +6,8 @@ import discord from contextlib import suppress from discord.ext import commands +from discord.utils import snowflake_time + from . import http from . import error from . import model @@ -56,6 +59,7 @@ def __init__(self, self.author = discord.User(data=_json["member"]["user"], state=self.bot._connection) else: self.author = discord.User(data=_json["user"], state=self.bot._connection) + self.created_at: datetime.datetime = snowflake_time(int(self.interaction_id)) @property def _deffered_hidden(self): diff --git a/discord_slash/model.py b/discord_slash/model.py index 2a1ddd0a9..26f5c7523 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -1,8 +1,13 @@ import asyncio +import datetime + import discord from enum import IntEnum from contextlib import suppress from inspect import iscoroutinefunction + +from discord.ext.commands import CooldownMapping, CommandOnCooldown + from . import http from . import error from . dpy_overrides import ComponentMessage @@ -132,38 +137,111 @@ def __init__(self, name, cmd): # Let's reuse old command formatting. if hasattr(self.func, '__commands_checks__'): self.__commands_checks__ = self.func.__commands_checks__ - async def invoke(self, *args): + cooldown = None + if hasattr(self.func, "__commands_cooldown__"): + cooldown = self.func.__commands_cooldown__ + self._buckets = CooldownMapping(cooldown) + + self._max_concurrency = None + if hasattr(self.func, "__commands_max_concurrency__"): + self._max_concurrency = self.func.__commands_max_concurrency__ + + def _prepare_cooldowns(self, ctx): + """ + Ref https://github.com/Rapptz/discord.py/blob/master/discord/ext/commands/core.py#L765 + """ + if self._buckets.valid: + dt = ctx.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + bucket = self._buckets.get_bucket(ctx, current) + retry_after = bucket.update_rate_limit(current) + if retry_after: + raise CommandOnCooldown(bucket, retry_after) + + async def _concurrency_checks(self, ctx): + """The checks required for cooldown and max concurrency.""" + # max concurrency checks + if self._max_concurrency is not None: + await self._max_concurrency.acquire(ctx) + try: + # cooldown checks + self._prepare_cooldowns(ctx) + except: + if self._max_concurrency is not None: + await self._max_concurrency.release(ctx) + raise + + async def invoke(self, *args, **kwargs): """ Invokes the command. :param args: Args for the command. :raises: .error.CheckFailure """ - args = list(args) - ctx = args.pop(0) - can_run = await self.can_run(ctx) + can_run = await self.can_run(args[0]) if not can_run: raise error.CheckFailure - coro = None # Get rid of annoying IDE complainings. + await self._concurrency_checks(args[0]) + + # to preventing needing different functions per object, + # this function simply handles cogs + if hasattr(self, "cog"): + return await self.func(self.cog, *args, **kwargs) + return await self.func(*args, **kwargs) + + def is_on_cooldown(self, ctx): + """Checks whether the command is currently on cooldown. + Ref https://github.com/Rapptz/discord.py/blob/master/discord/ext/commands/core.py#L797 + Parameters + ----------- + ctx: :class:`.Context` + The invocation context to use when checking the commands cooldown status. + Returns + -------- + :class:`bool` + A boolean indicating if the command is on cooldown. + """ + if not self._buckets.valid: + return False - not_kwargs = False - if args and isinstance(args[0], dict): - kwargs = args[0] - ctx.kwargs = kwargs - ctx.args = list(kwargs.values()) - try: - coro = self.func(ctx, **kwargs) - except TypeError: - args = list(kwargs.values()) - not_kwargs = True - else: - ctx.args = args - not_kwargs = True - if not_kwargs: - coro = self.func(ctx, *args) + bucket = self._buckets.get_bucket(ctx.message) + dt = ctx.message.edited_at or ctx.message.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + return bucket.get_tokens(current) == 0 + + def reset_cooldown(self, ctx): + """Resets the cooldown on this command. + Ref https://github.com/Rapptz/discord.py/blob/master/discord/ext/commands/core.py#L818 + Parameters + ----------- + ctx: :class:`.Context` + The invocation context to reset the cooldown under. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(ctx.message) + bucket.reset() + + def get_cooldown_retry_after(self, ctx): + """Retrieves the amount of seconds before this command can be tried again. + Ref https://github.com/Rapptz/discord.py/blob/master/discord/ext/commands/core.py#L830 + Parameters + ----------- + ctx: :class:`.Context` + The invocation context to retrieve the cooldown from. + Returns + -------- + :class:`float` + The amount of time left on this command's cooldown in seconds. + If this is ``0.0`` then the command isn't on cooldown. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(ctx.message) + dt = ctx.message.edited_at or ctx.message.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + return bucket.get_retry_after(current) - return await coro + return 0.0 def add_check(self, func): """ @@ -255,39 +333,6 @@ def __init__(self, *args): super().__init__(*args) self.cog = None # Manually set this later. - async def invoke(self, *args, **kwargs): - """ - Invokes the command. - - :param args: Args for the command. - :raises: .error.CheckFailure - """ - args = list(args) - ctx = args.pop(0) - can_run = await self.can_run(ctx) - if not can_run: - raise error.CheckFailure - - coro = None # Get rid of annoying IDE complainings. - - not_kwargs = False - if args and isinstance(args[0], dict): - kwargs = args[0] - ctx.kwargs = kwargs - ctx.args = list(kwargs.values()) - try: - coro = self.func(self.cog, ctx, **kwargs) - except TypeError: - args = list(kwargs.values()) - not_kwargs = True - else: - ctx.args = args - not_kwargs = True - if not_kwargs: - coro = self.func(self.cog, ctx, *args) - - return await coro - class CogSubcommandObject(SubcommandObject): """ @@ -302,39 +347,6 @@ def __init__(self, base, cmd, sub_group, name, sub): self.base_command_data = cmd self.cog = None # Manually set this later. - async def invoke(self, *args, **kwargs): - """ - Invokes the command. - - :param args: Args for the command. - :raises: .error.CheckFailure - """ - args = list(args) - ctx = args.pop(0) - can_run = await self.can_run(ctx) - if not can_run: - raise error.CheckFailure - - coro = None # Get rid of annoying IDE complainings. - - not_kwargs = False - if args and isinstance(args[0], dict): - kwargs = args[0] - ctx.kwargs = kwargs - ctx.args = list(kwargs.values()) - try: - coro = self.func(self.cog, ctx, **kwargs) - except TypeError: - args = list(kwargs.values()) - not_kwargs = True - else: - ctx.args = args - not_kwargs = True - if not_kwargs: - coro = self.func(self.cog, ctx, *args) - - return await coro - class SlashCommandOptionType(IntEnum): """ From c945187b0e01f6d5d1a4e4a11fb10696af825631 Mon Sep 17 00:00:00 2001 From: LordOfPolls Date: Wed, 2 Jun 2021 08:33:57 +0100 Subject: [PATCH 14/32] add error decorator support --- discord_slash/client.py | 7 +++++++ discord_slash/model.py | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/discord_slash/client.py b/discord_slash/client.py index 0ff6debc7..e391bfc10 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -875,6 +875,13 @@ 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: + 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) async def on_socket_response(self, msg): diff --git a/discord_slash/model.py b/discord_slash/model.py index 26f5c7523..662c09241 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -146,6 +146,14 @@ def __init__(self, name, cmd): # Let's reuse old command formatting. if hasattr(self.func, "__commands_max_concurrency__"): self._max_concurrency = self.func.__commands_max_concurrency__ + self.on_error = None + + def error(self, coro): + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The error handler must be a coroutine.") + self.on_error = coro + return coro + def _prepare_cooldowns(self, ctx): """ Ref https://github.com/Rapptz/discord.py/blob/master/discord/ext/commands/core.py#L765 @@ -296,7 +304,6 @@ def __init__(self, name, cmd): # Let's reuse old command formatting. self.default_permission = cmd["default_permission"] self.permissions = cmd["api_permissions"] or {} - class SubcommandObject(CommandObject): """ Subcommand object of this extension. From 6d1a2d8e3475fef79b6f8c1bbdd8e4ffc9eede20 Mon Sep 17 00:00:00 2001 From: LordOfPolls Date: Wed, 2 Jun 2021 08:51:13 +0100 Subject: [PATCH 15/32] add cog support for error dec --- 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 e391bfc10..82bc521f8 100644 --- a/discord_slash/client.py +++ b/discord_slash/client.py @@ -878,7 +878,10 @@ async def invoke_command(self, func, ctx, args): if hasattr(func, "on_error"): if func.on_error is not None: try: - await func.on_error(ctx, ex) + 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}") From 5f2921807f33cd098e2983c96081d6a0e5cf6d97 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Wed, 2 Jun 2021 14:58:28 +0300 Subject: [PATCH 16/32] Fix typo --- discord_slash/context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord_slash/context.py b/discord_slash/context.py index a2f80fc99..15845a94e 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -227,7 +227,7 @@ async def send(self, class SlashContext(InteractionContext): """ - Context of a slash command. Has all variables from :class:`InteractionContext`, plus the slash-command-specific ones below. + Context of a slash command. Has all attributes from :class:`InteractionContext`, plus the slash-command-specific ones below. :ivar name: Name of the command. :ivar args: List of processed arguments invoked with the command. @@ -253,7 +253,7 @@ def __init__(self, class ComponentContext(InteractionContext): """ - Context of a component interaction. Has all variables from :class:`InteractionContext`, plus the component-specific ones below. + Context of a component interaction. Has all attributes from :class:`InteractionContext`, plus the component-specific ones below. :ivar custom_id: The custom ID of the component. :ivar component_type: The type of the component. @@ -275,12 +275,12 @@ def __init__(self, self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, data=_json["message"]) - async def defer(self, hidden: bool = False, edit_origin = False): + async def defer(self, hidden: bool = False, edit_origin: bool = False): """ 'Defers' the response, showing a loading state to the user :param hidden: Whether the deferred response should be ephemeral . Default ``False``. - :param edit_origin: Whether the response is editting the origin message. If ``False``, the deferred response will be for a follow up message. Defaults ``False``. + :param edit_origin: Whether the response is editing the origin message. If ``False``, the deferred response will be for a follow up message. Defaults ``False``. """ if self.deferred or self.responded: raise error.AlreadyResponded("You have already responded to this command!") From 19c8fd3b555e63cd084e26cc1403ddd68f9fd497 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Wed, 2 Jun 2021 15:00:10 +0300 Subject: [PATCH 17/32] Updated project description and config --- README.md | 6 +++--- docs/conf.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fc90f7167..6a52dfecb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@


discord-py-slash-command -

A simple discord slash command handler for discord.py

+

A fork of a simple discord slash command handler for discord.py with support of discord components

Codacy Badge @@ -24,9 +24,9 @@ code and substituting its own for where it's needed. *discord-py-slash-command* slash command handler library to be made for Discord Bot API libraries. ## Installation -You are able to easily install the *discord-py-slash-command* library by using the given PIP line below: +You are able to easily install the *discord-py-interactions* library by using the given PIP line below: -`pip install -U discord-py-slash-command` +`pip install -U git+https://github.com/artem30801/discord-py-slash-command` ## Examples ### Quick Startup diff --git a/docs/conf.py b/docs/conf.py index 4fd63b95f..d3e410fc8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,9 +18,9 @@ # -- Project information ----------------------------------------------------- -project = 'discord-py-slash-command' -copyright = '2020-2021, eunwoo1104' -author = 'eunwoo1104' +project = 'discord-py-interactions' +copyright = '2020-2021, eunwoo1104+artem30801+hpenney2' +author = 'eunwoo1104+artem30801+hpenney2' # -- General configuration --------------------------------------------------- From d5c6f846a6d8575a455b9d78ae0e161f06e47992 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Thu, 3 Jun 2021 19:42:17 +0300 Subject: [PATCH 18/32] Fix typing syntax --- discord_slash/utils/manage_components.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 94890ff2c..add1a104e 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -139,7 +139,7 @@ def create_select_option(label: str, value: str, emoji=None, description: str = } -def create_select(options: list[dict], custom_id=None, placeholder=None, min_values=None, max_values=None): +def create_select(options: typing.List[dict], custom_id=None, placeholder=None, min_values=None, max_values=None): """ Creates a select (dropdown) component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). From 1b45c64875c3b07e5c430f05343ccec03b30f2d9 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Thu, 3 Jun 2021 19:50:16 +0300 Subject: [PATCH 19/32] Updated readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a52dfecb..36c7c54d6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ About ⦿ Installation ⦿ Examples ⦿ - Documentation ⦿ + Documentation on the fork ⦿ Discord Server

@@ -24,9 +24,9 @@ code and substituting its own for where it's needed. *discord-py-slash-command* slash command handler library to be made for Discord Bot API libraries. ## Installation -You are able to easily install the *discord-py-interactions* library by using the given PIP line below: +You are able to easily install this *discord-py-interactions* library fork by using the given PIP line below: -`pip install -U git+https://github.com/artem30801/discord-py-slash-command` +`pip install -U git+https://github.com/artem30801/discord-py-interactions` ## Examples ### Quick Startup From 542e3bd474c432150a6f269daa72029522b1910a Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sat, 5 Jun 2021 16:58:56 +0300 Subject: [PATCH 20/32] Reverted readme and docs attribution changes --- README.md | 10 +++++----- docs/conf.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 36c7c54d6..2640437ed 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@


discord-py-slash-command -

A fork of a simple discord slash command handler for discord.py with support of discord components

+

A simple discord slash command handler for discord.py

Codacy Badge @@ -11,7 +11,7 @@ About ⦿ Installation ⦿ Examples ⦿ - Documentation on the fork ⦿ + Documentation ⦿ Discord Server

@@ -24,9 +24,9 @@ code and substituting its own for where it's needed. *discord-py-slash-command* slash command handler library to be made for Discord Bot API libraries. ## Installation -You are able to easily install this *discord-py-interactions* library fork by using the given PIP line below: +You are able to easily install the *discord-py-slash-command* library by using the given PIP line below: -`pip install -U git+https://github.com/artem30801/discord-py-interactions` +`pip install -U discord-py-slash-command` ## Examples ### Quick Startup @@ -83,4 +83,4 @@ This library is based on gateway event. If you are looking for webserver based, [dispike](https://github.com/ms7m/dispike) [discord-interactions-python](https://github.com/discord/discord-interactions-python) Or for other languages: -[discord-api-docs Community Resources: Interactions](https://discord.com/developers/docs/topics/community-resources#interactions) +[discord-api-docs Community Resources: Interactions](https://discord.com/developers/docs/topics/community-resources#interactions) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index d3e410fc8..8cb9de81e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,9 +18,9 @@ # -- Project information ----------------------------------------------------- -project = 'discord-py-interactions' -copyright = '2020-2021, eunwoo1104+artem30801+hpenney2' -author = 'eunwoo1104+artem30801+hpenney2' +project = 'discord-py-slash-command' +copyright = '2020-2021, eunwoo1104' +author = 'eunwoo1104' # -- General configuration --------------------------------------------------- @@ -68,4 +68,4 @@ intersphinx_mapping = { 'py': ('https://docs.python.org/3', None), 'discord': ("https://discordpy.readthedocs.io/en/latest/", None) -} +} \ No newline at end of file From a840c2d72322f62d8e3c8e6589a2573a75e304b3 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 00:33:35 +0300 Subject: [PATCH 21/32] Added venv to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 839b32575..91c908279 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ slash.log test __*.py soontm.png +venv/ # Distribution / packaging .Python From 4e2c31057383d986e655f809f077097bb7b11032 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 00:35:04 +0300 Subject: [PATCH 22/32] Added support for actionrows in wait_for_component --- discord_slash/utils/manage_components.py | 33 +++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index add1a104e..aef65bdc6 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -159,23 +159,49 @@ def create_select(options: typing.List[dict], custom_id=None, placeholder=None, } -async def wait_for_component(client: discord.Client, component: typing.Union[dict, str], check=None, timeout=None) \ +def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Generator[str]: + """ + Returns generator with 'custom_id' of component or components. + + :param component: Custom ID or component dict (actionrow or button) or list of previous two. + """ + + if isinstance(component, str): + yield component + elif isinstance(component, dict): + if component["type"] == ComponentsType.actionrow: + yield from (comp["custom_id"] for comp in component["components"]) + else: + yield component["custom_id"] + elif isinstance(component, list): + # Either list of components (actionrows or buttons) or list of ids + yield from (comp_id for comp in component for comp_id in get_components_ids(comp)) + else: + raise IncorrectFormat(f"Unknown component type of {component} ({type(component)}). " + f"Expected str, dict or list") + + +async def wait_for_component(client: discord.Client, component: typing.Union[str, dict, list], check=None, timeout=None) \ -> ComponentContext: """ Waits for a component interaction. Only accepts interactions based on the custom ID of the component, and optionally a check function. :param client: The client/bot object. :type client: :class:`discord.Client` - :param component: The component dict or custom ID. + :param component: Custom ID or component dict (actionrow or button) or list of previous two. :type component: Union[dict, str] :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` """ + + components_ids = list(get_components_ids(component)) + def _check(ctx): if check and not check(ctx): return False - return (component["custom_id"] if isinstance(component, dict) else component) == ctx.custom_id + wanted_component = ctx.custom_id in components_ids or not components_ids # if matches or components_ids empty + return wanted_component return await client.wait_for("component", check=_check, timeout=timeout) @@ -193,6 +219,7 @@ async def wait_for_any_component(client: discord.Client, message: typing.Union[d :param timeout: The number of seconds to wait before timing out and raising :exc:`asyncio.TimeoutError`. :raises: :exc:`asyncio.TimeoutError` """ + def _check(ctx): if check and not check(ctx): return False From f4e34ff7db92311cdc0d0ebb578d3eba41794c02 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 16:53:19 +0300 Subject: [PATCH 23/32] Applied black formatting --- discord_slash/utils/manage_components.py | 67 ++++++++++++++++-------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index aef65bdc6..c8eeeb80b 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -21,13 +21,13 @@ def create_actionrow(*components: dict) -> dict: """ if not components or len(components) > 5: raise IncorrectFormat("Number of components in one row should be between 1 and 5.") - if ComponentsType.select in [component["type"] for component in components] and len(components) > 1: + if ( + ComponentsType.select in [component["type"] for component in components] + and len(components) > 1 + ): raise IncorrectFormat("Action row must have only one select component and nothing else") - return { - "type": ComponentsType.actionrow, - "components": components - } + return {"type": ComponentsType.actionrow, "components": components} class ButtonStyle(enum.IntEnum): @@ -59,12 +59,14 @@ def emoji_to_dict(emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str]) return emoji if emoji else {} -def create_button(style: typing.Union[ButtonStyle, int], - label: str = None, - emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str] = None, - custom_id: str = None, - url: str = None, - disabled: bool = False) -> dict: +def create_button( + style: typing.Union[ButtonStyle, int], + label: str = None, + emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str] = None, + custom_id: str = None, + url: str = None, + disabled: bool = False, +) -> dict: """ Creates a button component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). @@ -118,7 +120,9 @@ def create_button(style: typing.Union[ButtonStyle, int], return data -def create_select_option(label: str, value: str, emoji=None, description: str = None, default: bool = False): +def create_select_option( + label: str, value: str, emoji=None, description: str = None, default: bool = False +): """ Creates an option for select components. @@ -135,11 +139,17 @@ def create_select_option(label: str, value: str, emoji=None, description: str = "value": value, "description": description, "default": default, - "emoji": emoji + "emoji": emoji, } -def create_select(options: typing.List[dict], custom_id=None, placeholder=None, min_values=None, max_values=None): +def create_select( + options: typing.List[dict], + custom_id=None, + placeholder=None, + min_values=None, + max_values=None, +): """ Creates a select (dropdown) component for use with the ``components`` field. Must be inside an ActionRow to be used (see :meth:`create_actionrow`). @@ -177,12 +187,18 @@ def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Gener # Either list of components (actionrows or buttons) or list of ids yield from (comp_id for comp in component for comp_id in get_components_ids(comp)) else: - raise IncorrectFormat(f"Unknown component type of {component} ({type(component)}). " - f"Expected str, dict or list") + raise IncorrectFormat( + f"Unknown component type of {component} ({type(component)}). " + f"Expected str, dict or list" + ) -async def wait_for_component(client: discord.Client, component: typing.Union[str, dict, list], check=None, timeout=None) \ - -> ComponentContext: +async def wait_for_component( + client: discord.Client, + component: typing.Union[str, dict, list], + check=None, + timeout=None, +) -> ComponentContext: """ Waits for a component interaction. Only accepts interactions based on the custom ID of the component, and optionally a check function. @@ -200,14 +216,19 @@ async def wait_for_component(client: discord.Client, component: typing.Union[str def _check(ctx): if check and not check(ctx): return False - wanted_component = ctx.custom_id in components_ids or not components_ids # if matches or components_ids empty + # if matches or components_ids empty + wanted_component = ctx.custom_id in components_ids or not components_ids return wanted_component return await client.wait_for("component", check=_check, timeout=timeout) -async def wait_for_any_component(client: discord.Client, message: typing.Union[discord.Message, int], - check=None, timeout=None) -> ComponentContext: +async def wait_for_any_component( + client: discord.Client, + message: typing.Union[discord.Message, int], + check=None, + timeout=None, +) -> ComponentContext: """ Waits for any component interaction. Only accepts interactions based on the message ID given and optionally a check function. @@ -223,6 +244,8 @@ async def wait_for_any_component(client: discord.Client, message: typing.Union[d def _check(ctx): if check and not check(ctx): return False - return (message.id if isinstance(message, discord.Message) else message) == ctx.origin_message_id + return ( + message.id if isinstance(message, discord.Message) else message + ) == ctx.origin_message_id return await client.wait_for("component", check=_check, timeout=timeout) From 06f34374a4881c883c0790214decd5dd9e91f24c Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 17:18:30 +0300 Subject: [PATCH 24/32] Added message kwarg to wait_for_component, removed wait_for_any_component --- discord_slash/utils/manage_components.py | 64 +++++++++++------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index c8eeeb80b..fa7932d08 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -169,7 +169,7 @@ def create_select( } -def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Generator[str]: +def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Iterator[str]: """ Returns generator with 'custom_id' of component or components. @@ -193,59 +193,53 @@ def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Gener ) +def _get_messages_ids(message: typing.Union[discord.Message, int, list]) -> 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)) + else: + raise IncorrectFormat( + f"Unknown component type of {message} ({type(message)}). " + f"Expected discord.Message, int or list" + ) + + async def wait_for_component( client: discord.Client, - component: typing.Union[str, dict, list], + component: typing.Union[str, dict, list] = None, + message: typing.Union[discord.Message, int, list] = None, check=None, timeout=None, ) -> ComponentContext: """ - Waits for a component interaction. Only accepts interactions based on the custom ID of the component, and optionally a check function. + Helper function - wrapper around 'client.wait_for("component", ...)' + Waits for a component interaction. Only accepts interactions based on the custom ID of the component or/and message ID, and optionally a check function. :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 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` """ - components_ids = list(get_components_ids(component)) - - def _check(ctx): - if check and not check(ctx): - return False - # if matches or components_ids empty - wanted_component = ctx.custom_id in components_ids or not components_ids - return wanted_component - - return await client.wait_for("component", check=_check, timeout=timeout) - + if not (component or message): + raise IncorrectFormat("You must specify component or message (or both)") -async def wait_for_any_component( - client: discord.Client, - message: typing.Union[discord.Message, int], - check=None, - timeout=None, -) -> ComponentContext: - """ - Waits for any component interaction. Only accepts interactions based on the message ID given and optionally a check function. - - :param client: The client/bot object. - :type client: :class:`discord.Client` - :param message: The message object to check for, or the message ID. - :type message: Union[discord.Message, int] - :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` - """ + components_ids = list(get_components_ids(component)) if component else None + message_ids = list(_get_messages_ids(message)) if message else None - def _check(ctx): + def _check(ctx: ComponentContext): if check and not check(ctx): return False - return ( - message.id if isinstance(message, discord.Message) else message - ) == ctx.origin_message_id + # if components_ids is empty or there is a match + wanted_component = not components_ids or ctx.custom_id in components_ids + wanted_message = not message_ids or ctx.origin_message_id in message_ids + return wanted_component and wanted_message return await client.wait_for("component", check=_check, timeout=timeout) From 296792f433c578a3860cc98e0120650bef429de2 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 17:43:12 +0300 Subject: [PATCH 25/32] Exception for hidden+edit_origin --- discord_slash/context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/discord_slash/context.py b/discord_slash/context.py index fa56a10cb..5b7fb52c4 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -289,7 +289,9 @@ async def defer(self, hidden: bool = False, edit_origin: bool = False): if self.deferred or self.responded: raise error.AlreadyResponded("You have already responded to this command!") base = {"type": 6 if edit_origin else 5} - if hidden and not edit_origin: + if hidden: + if edit_origin: + raise error.IncorrectFormat("'hidden' and 'edit_origin' flags are mutually exclusive") base["data"] = {"flags": 64} self._deferred_hidden = True await self._http.post_initial_response(base, self.interaction_id, self._token) From 1d33e7f5d187284c7e06e0952cabafbebd5b9459 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 17:49:09 +0300 Subject: [PATCH 26/32] Warning on edit_origin when deferred with different state --- discord_slash/context.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/discord_slash/context.py b/discord_slash/context.py index 5b7fb52c4..1e0529043 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -275,6 +275,8 @@ def __init__(self, self.origin_message = None self.origin_message_id = int(_json["message"]["id"]) if "message" in _json.keys() else None + self._deferred_edit_origin = False + if self.origin_message_id and (_json["message"]["flags"] & 64) != 64: self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, data=_json["message"]) @@ -288,12 +290,17 @@ async def defer(self, hidden: bool = False, edit_origin: bool = False): """ if self.deferred or self.responded: raise error.AlreadyResponded("You have already responded to this command!") + base = {"type": 6 if edit_origin else 5} + if hidden: if edit_origin: raise error.IncorrectFormat("'hidden' and 'edit_origin' flags are mutually exclusive") base["data"] = {"flags": 64} self._deferred_hidden = True + + self._deferred_edit_origin = edit_origin + await self._http.post_initial_response(base, self.interaction_id, self._token) self.deferred = True @@ -340,6 +347,11 @@ async def edit_origin(self, **fields): if files and not self.deferred: await self.defer(edit_origin=True) if self.deferred: + if not self._deferred_edit_origin: + self._logger.warning( + "Deferred response might not be what you set it to! (edit origin / send response message) " + "This is because it was deferred in a different state." + ) _json = await self._http.edit(_resp, self._token, files=files) self.deferred = False else: From 3eb8c27432832d9596b478a6611e6014552ce8e4 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 17:49:52 +0300 Subject: [PATCH 27/32] Changed exception types in get_components_ids and _get_messages_ids --- discord_slash/utils/manage_components.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index fa7932d08..314632996 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -3,7 +3,7 @@ import typing import discord from ..context import ComponentContext -from ..error import IncorrectFormat +from ..error import IncorrectFormat, IncorrectType class ComponentsType(enum.IntEnum): @@ -187,7 +187,7 @@ def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Itera # Either list of components (actionrows or buttons) or list of ids yield from (comp_id for comp in component for comp_id in get_components_ids(comp)) else: - raise IncorrectFormat( + raise IncorrectType( f"Unknown component type of {component} ({type(component)}). " f"Expected str, dict or list" ) @@ -201,7 +201,7 @@ def _get_messages_ids(message: typing.Union[discord.Message, int, list]) -> typi elif isinstance(message, list): yield from (msg_id for msg in message for msg_id in _get_messages_ids(msg)) else: - raise IncorrectFormat( + raise IncorrectType( f"Unknown component type of {message} ({type(message)}). " f"Expected discord.Message, int or list" ) From 9e242eff1027606ce47408d9772e712364bb87b1 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 17:53:40 +0300 Subject: [PATCH 28/32] Added warning for send when deffered with different state --- discord_slash/context.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/discord_slash/context.py b/discord_slash/context.py index 1e0529043..960fef868 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -304,6 +304,24 @@ async def defer(self, hidden: bool = False, edit_origin: bool = False): await self._http.post_initial_response(base, self.interaction_id, self._token) self.deferred = True + async def send(self, + content: str = "", *, + embed: discord.Embed = None, + embeds: typing.List[discord.Embed] = None, + tts: bool = False, + file: discord.File = None, + files: typing.List[discord.File] = None, + allowed_mentions: discord.AllowedMentions = None, + hidden: bool = False, + delete_after: float = None, + components: typing.List[dict] = None, + ) -> model.SlashMessage: + if self.deferred and self._deferred_edit_origin: + self._logger.warning( + "Deferred response might not be what you set it to! (edit origin / send response message) " + "This is because it was deferred with different response type." + ) + async def edit_origin(self, **fields): """ Edits the origin message of the component. @@ -350,7 +368,7 @@ async def edit_origin(self, **fields): if not self._deferred_edit_origin: self._logger.warning( "Deferred response might not be what you set it to! (edit origin / send response message) " - "This is because it was deferred in a different state." + "This is because it was deferred with different response type." ) _json = await self._http.edit(_resp, self._token, files=files) self.deferred = False From 0ecd9222acfbdca2f70de7a36b37f1c127d54cbd Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 17:58:32 +0300 Subject: [PATCH 29/32] Tweaked docstrings --- discord_slash/context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord_slash/context.py b/discord_slash/context.py index 960fef868..e2363e2ad 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -17,7 +17,7 @@ class InteractionContext: """ Base context for interactions.\n - Kinda similar with discord.ext.commands.Context. + In some ways similar with discord.ext.commands.Context. .. warning:: Do not manually init this model. @@ -127,7 +127,7 @@ async def send(self, components: typing.List[dict] = None, ) -> model.SlashMessage: """ - Sends response of the slash command. + Sends response of the interaction. .. warning:: - Since Release 1.0.9, this is completely changed. If you are migrating from older version, please make sure to fix the usage. @@ -286,7 +286,7 @@ async def defer(self, hidden: bool = False, edit_origin: bool = False): 'Defers' the response, showing a loading state to the user :param hidden: Whether the deferred response should be ephemeral . Default ``False``. - :param edit_origin: Whether the response is editing the origin message. If ``False``, the deferred response will be for a follow up message. Defaults ``False``. + :param edit_origin: Whether the type is editing the origin message. If ``False``, the deferred response will be for a follow up message. Defaults ``False``. """ if self.deferred or self.responded: raise error.AlreadyResponded("You have already responded to this command!") From a664f4abe349ab7fa0a37414d69a489e387cdce3 Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 19:14:06 +0300 Subject: [PATCH 30/32] Moved component enums to model.py --- discord_slash/__init__.py | 2 +- discord_slash/model.py | 21 ++++++++++++++++ discord_slash/utils/manage_components.py | 32 +++++------------------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index 662495e71..1d4574676 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -9,7 +9,7 @@ """ from .client import SlashCommand -from .model import SlashCommandOptionType +from .model import SlashCommandOptionType, ComponentType, ButtonStyle from .context import SlashContext from .context import ComponentContext from .dpy_overrides import ComponentMessage diff --git a/discord_slash/model.py b/discord_slash/model.py index df9f7021b..54d340c82 100644 --- a/discord_slash/model.py +++ b/discord_slash/model.py @@ -531,3 +531,24 @@ class SlashCommandPermissionType(IntEnum): def from_type(cls, t: type): if issubclass(t, discord.abc.Role): return cls.ROLE if issubclass(t, discord.abc.User): return cls.USER + + +class ComponentType(IntEnum): + actionrow = 1 + button = 2 + select = 3 + + +class ButtonStyle(IntEnum): + blue = 1 + blurple = 1 + gray = 2 + grey = 2 + green = 3 + red = 4 + URL = 5 + + primary = 1 + secondary = 2 + success = 3 + danger = 4 diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index 314632996..12179f36f 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -4,12 +4,7 @@ import discord from ..context import ComponentContext from ..error import IncorrectFormat, IncorrectType - - -class ComponentsType(enum.IntEnum): - actionrow = 1 - button = 2 - select = 3 +from ..model import ComponentType, ButtonStyle def create_actionrow(*components: dict) -> dict: @@ -22,27 +17,12 @@ def create_actionrow(*components: dict) -> dict: if not components or len(components) > 5: raise IncorrectFormat("Number of components in one row should be between 1 and 5.") if ( - ComponentsType.select in [component["type"] for component in components] + ComponentType.select in [component["type"] for component in components] and len(components) > 1 ): raise IncorrectFormat("Action row must have only one select component and nothing else") - return {"type": ComponentsType.actionrow, "components": components} - - -class ButtonStyle(enum.IntEnum): - blue = 1 - blurple = 1 - gray = 2 - grey = 2 - green = 3 - red = 4 - URL = 5 - - primary = 1 - secondary = 2 - success = 3 - danger = 4 + return {"type": ComponentType.actionrow, "components": components} def emoji_to_dict(emoji: typing.Union[discord.Emoji, discord.PartialEmoji, str]) -> dict: @@ -101,7 +81,7 @@ def create_button( emoji = emoji_to_dict(emoji) data = { - "type": ComponentsType.button, + "type": ComponentType.button, "style": style, } @@ -160,7 +140,7 @@ def create_select( raise IncorrectFormat("Options length should be between 1 and 25.") return { - "type": ComponentsType.select, + "type": ComponentType.select, "options": options, "custom_id": custom_id or str(uuid.uuid4()), "placeholder": placeholder or "", @@ -179,7 +159,7 @@ def get_components_ids(component: typing.Union[str, dict, list]) -> typing.Itera if isinstance(component, str): yield component elif isinstance(component, dict): - if component["type"] == ComponentsType.actionrow: + if component["type"] == ComponentType.actionrow: yield from (comp["custom_id"] for comp in component["components"]) else: yield component["custom_id"] From 7f62aa66681a86919db6c6ac71e7fbf2e0e88b7e Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Sun, 6 Jun 2021 23:03:41 +0300 Subject: [PATCH 31/32] Applied pre_push --- discord_slash/__init__.py | 6 +- discord_slash/context.py | 108 +++++++++++++---------- discord_slash/utils/manage_components.py | 3 +- docs/conf.py | 2 +- 4 files changed, 68 insertions(+), 51 deletions(-) diff --git a/discord_slash/__init__.py b/discord_slash/__init__.py index 1e64aceb8..75b207e76 100644 --- a/discord_slash/__init__.py +++ b/discord_slash/__init__.py @@ -10,11 +10,11 @@ from .client import SlashCommand # noqa: F401 from .const import __version__ # noqa: F401 -from .context import SlashContext # noqa: F401 from .context import ComponentContext # noqa: F401 +from .context import SlashContext # noqa: F401 from .dpy_overrides import ComponentMessage # noqa: F401 -from .model import SlashCommandOptionType # noqa: F401 -from .model import ComponentType # noqa: F401 from .model import ButtonStyle # noqa: F401 +from .model import ComponentType # noqa: F401 +from .model import SlashCommandOptionType # noqa: F401 from .utils import manage_commands # noqa: F401 from .utils import manage_components # noqa: F401 diff --git a/discord_slash/context.py b/discord_slash/context.py index 9f2f6f718..6de76b412 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -9,6 +9,7 @@ from . import error, http, model from .dpy_overrides import ComponentMessage + class InteractionContext: """ Base context for interactions.\n @@ -183,14 +184,19 @@ async def send( if delete_after and hidden: raise error.IncorrectFormat("You can't delete a hidden message!") if components and not all(comp.get("type") == 1 for comp in components): - raise error.IncorrectFormat("The top level of the components list must be made of ActionRows!") + raise error.IncorrectFormat( + "The top level of the components list must be made of ActionRows!" + ) base = { "content": content, "tts": tts, "embeds": [x.to_dict() for x in embeds] if embeds else [], - "allowed_mentions": allowed_mentions.to_dict() if allowed_mentions - else self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {}, + "allowed_mentions": allowed_mentions.to_dict() + if allowed_mentions + else self.bot.allowed_mentions.to_dict() + if self.bot.allowed_mentions + else {}, "components": components or [], } if hidden: @@ -210,10 +216,7 @@ async def send( resp = await self._http.edit(base, self._token, files=files) self.deferred = False else: - json_data = { - "type": 4, - "data": base - } + json_data = {"type": 4, "data": base} await self._http.post_initial_response(json_data, self.interaction_id, self._token) if not hidden: resp = await self._http.edit({}, self._token) @@ -226,11 +229,13 @@ async def send( for file in files: file.close() if not hidden: - smsg = model.SlashMessage(state=self.bot._connection, - data=resp, - channel=self.channel or discord.Object(id=self.channel_id), - _http=self._http, - interaction_token=self._token) + smsg = model.SlashMessage( + state=self.bot._connection, + data=resp, + channel=self.channel or discord.Object(id=self.channel_id), + _http=self._http, + interaction_token=self._token, + ) if delete_after: self.bot.loop.create_task(smsg.delete(delay=delete_after)) if initial_message: @@ -251,11 +256,14 @@ class SlashContext(InteractionContext): :ivar subcommand_group: Subcommand group of the command. :ivar command_id: ID of the command. """ - def __init__(self, - _http: http.SlashCommandRequest, - _json: dict, - _discord: typing.Union[discord.Client, commands.Bot], - logger): + + def __init__( + self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger, + ): self.name = self.command = self.invoked_with = _json["data"]["name"] self.args = [] self.kwargs = {} @@ -275,11 +283,14 @@ class ComponentContext(InteractionContext): :ivar origin_message: The origin message of the component. Not available if the origin message was ephemeral. :ivar origin_message_id: The ID of the origin message. """ - def __init__(self, - _http: http.SlashCommandRequest, - _json: dict, - _discord: typing.Union[discord.Client, commands.Bot], - logger): + + def __init__( + self, + _http: http.SlashCommandRequest, + _json: dict, + _discord: typing.Union[discord.Client, commands.Bot], + logger, + ): self.custom_id = self.component_id = _json["data"]["custom_id"] self.component_type = _json["data"]["component_type"] super().__init__(_http=_http, _json=_json, _discord=_discord, logger=logger) @@ -289,8 +300,9 @@ def __init__(self, self._deferred_edit_origin = False if self.origin_message_id and (_json["message"]["flags"] & 64) != 64: - self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel, - data=_json["message"]) + self.origin_message = ComponentMessage( + state=self.bot._connection, channel=self.channel, data=_json["message"] + ) async def defer(self, hidden: bool = False, edit_origin: bool = False): """ @@ -306,7 +318,9 @@ async def defer(self, hidden: bool = False, edit_origin: bool = False): if hidden: if edit_origin: - raise error.IncorrectFormat("'hidden' and 'edit_origin' flags are mutually exclusive") + raise error.IncorrectFormat( + "'hidden' and 'edit_origin' flags are mutually exclusive" + ) base["data"] = {"flags": 64} self._deferred_hidden = True @@ -315,18 +329,20 @@ async def defer(self, hidden: bool = False, edit_origin: bool = False): await self._http.post_initial_response(base, self.interaction_id, self._token) self.deferred = True - async def send(self, - content: str = "", *, - embed: discord.Embed = None, - embeds: typing.List[discord.Embed] = None, - tts: bool = False, - file: discord.File = None, - files: typing.List[discord.File] = None, - allowed_mentions: discord.AllowedMentions = None, - hidden: bool = False, - delete_after: float = None, - components: typing.List[dict] = None, - ) -> model.SlashMessage: + async def send( + self, + content: str = "", + *, + embed: discord.Embed = None, + embeds: typing.List[discord.Embed] = None, + tts: bool = False, + file: discord.File = None, + files: typing.List[discord.File] = None, + allowed_mentions: discord.AllowedMentions = None, + hidden: bool = False, + delete_after: float = None, + components: typing.List[dict] = None, + ) -> model.SlashMessage: if self.deferred and self._deferred_edit_origin: self._logger.warning( "Deferred response might not be what you set it to! (edit origin / send response message) " @@ -369,8 +385,13 @@ async def edit_origin(self, **fields): _resp["embeds"] = [x.to_dict() for x in embeds] allowed_mentions = fields.get("allowed_mentions") - _resp["allowed_mentions"] = allowed_mentions.to_dict() if allowed_mentions else \ - self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {} + _resp["allowed_mentions"] = ( + allowed_mentions.to_dict() + if allowed_mentions + else self.bot.allowed_mentions.to_dict() + if self.bot.allowed_mentions + else {} + ) if not self.responded: if files and not self.deferred: @@ -381,14 +402,11 @@ async def edit_origin(self, **fields): "Deferred response might not be what you set it to! (edit origin / send response message) " "This is because it was deferred with different response type." ) - _json = await self._http.edit(_resp, self._token, files=files) + await self._http.edit(_resp, self._token, files=files) self.deferred = False else: - json_data = { - "type": 7, - "data": _resp - } - _json = await self._http.post_initial_response(json_data, self.interaction_id, self._token) + json_data = {"type": 7, "data": _resp} + await self._http.post_initial_response(json_data, self.interaction_id, self._token) self.responded = True else: raise error.IncorrectFormat("Already responded") diff --git a/discord_slash/utils/manage_components.py b/discord_slash/utils/manage_components.py index ce750c534..60f0bfbd1 100644 --- a/discord_slash/utils/manage_components.py +++ b/discord_slash/utils/manage_components.py @@ -1,4 +1,3 @@ -import enum import typing import uuid @@ -6,7 +5,7 @@ from ..context import ComponentContext from ..error import IncorrectFormat, IncorrectType -from ..model import ComponentType, ButtonStyle +from ..model import ButtonStyle, ComponentType def create_actionrow(*components: dict) -> dict: diff --git a/docs/conf.py b/docs/conf.py index 9f2c81420..1e77fbb77 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -66,4 +66,4 @@ intersphinx_mapping = { "py": ("https://docs.python.org/3", None), "discord": ("https://discordpy.readthedocs.io/en/latest/", None), -} \ No newline at end of file +} From 3a6dee21c313b1719689ffd9d43d242c08421b8e Mon Sep 17 00:00:00 2001 From: artem30801 <38689676+artem30801@users.noreply.github.com> Date: Mon, 7 Jun 2021 15:42:50 +0300 Subject: [PATCH 32/32] Fix ComponentContext.send() --- discord_slash/context.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/discord_slash/context.py b/discord_slash/context.py index 6de76b412..29d2501fc 100644 --- a/discord_slash/context.py +++ b/discord_slash/context.py @@ -348,6 +348,18 @@ async def send( "Deferred response might not be what you set it to! (edit origin / send response message) " "This is because it was deferred with different response type." ) + return await super().send( + content, + embed=embed, + embeds=embeds, + tts=tts, + file=file, + files=files, + allowed_mentions=allowed_mentions, + hidden=hidden, + delete_after=delete_after, + components=components, + ) async def edit_origin(self, **fields): """