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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 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/20] 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/20] 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
@@ -11,7 +11,7 @@
About ⦿
Installation ⦿
Examples ⦿
- Documentation on the fork ⦿
+ Documentation ⦿
Discord Server