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 @@
-
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
@@ -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 @@
-
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
@@ -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):
"""