Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d4861ba
Added components support
artem30801 May 30, 2021
4b21542
Added manage_components to init
artem30801 May 30, 2021
2df0fdf
Added default emoji support to buttons
artem30801 May 30, 2021
3f4c9b4
Bump version
artem30801 May 30, 2021
0946f89
Fixed wait_for_any_component, added method to edit component message …
artem30801 May 30, 2021
c451cff
Creating unspecified custom_id for buttons as str of uuid
artem30801 May 30, 2021
5f98425
Added select and select-option generation functions
artem30801 May 31, 2021
a783791
Updated button generation code
artem30801 Jun 1, 2021
a6e8ac4
Fixed processing component context from ephemeral message
artem30801 Jun 1, 2021
5dbaee7
Add/edit docs for new stuff + code edits
hpenney2 Jun 1, 2021
87c9ee6
Better wait_for_component and wait_for_any_component parameters
hpenney2 Jun 1, 2021
553d85b
Fix docs for origin_message_id
hpenney2 Jun 1, 2021
db7ac58
Merge pull request #2 from hpenney2/artem-fork
artem30801 Jun 1, 2021
1a1f85b
Add cooldown and max conc support ++
LordOfPolls Jun 2, 2021
c945187
add error decorator support
LordOfPolls Jun 2, 2021
6d1a2d8
add cog support for error dec
LordOfPolls Jun 2, 2021
5f29218
Fix typo
artem30801 Jun 2, 2021
19c8fd3
Updated project description and config
artem30801 Jun 2, 2021
e99851c
Merge pull request #3 from LordOfPolls/master
artem30801 Jun 2, 2021
832f61f
Merge branch 'eunwoo1104:master' into master
artem30801 Jun 2, 2021
d5c6f84
Fix typing syntax
artem30801 Jun 3, 2021
1b45c64
Updated readme
artem30801 Jun 3, 2021
94cb1b2
Merge remote-tracking branch 'origin/master'
artem30801 Jun 3, 2021
ad1c867
Merge branch 'master' into pr/4
artem30801 Jun 5, 2021
542e3bd
Reverted readme and docs attribution changes
artem30801 Jun 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions discord_slash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
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

__version__ = "1.2.2"
29 changes: 26 additions & 3 deletions discord_slash/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from . import model
from . import error
from . import context
from . import dpy_overrides
from .utils import manage_commands


Expand Down Expand Up @@ -869,8 +870,21 @@ 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:
if hasattr(func, "on_error"):
if func.on_error is not None:
try:
if hasattr(func, "cog"):
await func.on_error(func.cog, ctx, ex)
else:
await func.on_error(ctx, ex)
return
except Exception as e:
self.logger.error(f"{ctx.command}:: Error using error decorator: {e}")
await self.on_slash_command_error(ctx, ex)

async def on_socket_response(self, msg):
Expand All @@ -886,10 +900,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)

raise NotImplementedError

if to_use["type"] not in (1, 2):
return # to only process ack and slash-commands and exclude other interactions like buttons
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)
Expand Down
176 changes: 153 additions & 23 deletions discord_slash/context.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import datetime
import typing
import asyncio
from warnings import warn

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
from . dpy_overrides import ComponentMessage


class SlashContext:
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.
Expand All @@ -43,15 +41,9 @@ 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.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
Expand All @@ -67,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):
Expand Down Expand Up @@ -118,7 +111,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,
Expand All @@ -130,7 +123,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.

Expand All @@ -157,6 +152,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:
Expand All @@ -174,13 +171,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
Expand All @@ -196,21 +196,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()
Expand All @@ -219,11 +219,141 @@ 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:
self.message = smsg
return smsg
else:
return resp


class SlashContext(InteractionContext):
"""
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.
: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,
_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):
"""
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.
: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):
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 and (_json["message"]["flags"] & 64) != 64:
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):
"""
'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``.
"""
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):
"""
Edits the origin message of the component.
Refer to :meth:`discord.Message.edit` and :meth:`InteractionContext.send` for fields.
"""
_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:
_json = 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)
self.responded = True
else:
raise error.IncorrectFormat("Already responded")

if files:
for file in files:
file.close()

# 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)
Loading