Skip to content

Commit 8e7e518

Browse files
Merge branch 'master' into anothercat/prevent-uneeded-requests-autoregister
2 parents 460aa7b + c107539 commit 8e7e518

File tree

13 files changed

+221
-103
lines changed

13 files changed

+221
-103
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ What changes were made?
1313
- Issue:
1414
- [ ] This adds something new.
1515
- [ ] There is/are breaking change(s).
16-
- [ ] (If required) Relavent documentation has been updated/added.
16+
- [ ] (If required) Relevant documentation has been updated/added.
1717
- [ ] This is not a code change. (README, docs, etc.)

discord_slash/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@
1313
from .context import SlashContext
1414
from .utils import manage_commands
1515

16-
__version__ = "1.0.9.5"
16+
__version__ = "1.1.1"

discord_slash/client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,12 +729,15 @@ async def invoke_command(self, func, ctx, args):
729729
try:
730730
not_kwargs = False
731731
if isinstance(args, dict):
732+
ctx.kwargs = args
733+
ctx.args = list(args.values())
732734
try:
733735
await func.invoke(ctx, **args)
734736
except TypeError:
735737
args = list(args.values())
736738
not_kwargs = True
737739
else:
740+
ctx.args = args
738741
not_kwargs = True
739742
if not_kwargs:
740743
await func.invoke(ctx, *args)
@@ -873,5 +876,5 @@ async def on_slash_command_error(ctx, ex):
873876
if hasattr(self._discord, "on_slash_command_error"):
874877
self._discord.dispatch("slash_command_error", ctx, ex)
875878
return
876-
# Prints exception if not overrided or has no listener for error.
879+
# Prints exception if not overridden or has no listener for error.
877880
self.logger.exception(f"An exception has occurred while executing command `{ctx.name}`:")

discord_slash/context.py

Lines changed: 93 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import typing
22
import asyncio
3+
from warnings import warn
4+
35
import discord
46
from contextlib import suppress
57
from discord.ext import commands
@@ -18,14 +20,18 @@ class SlashContext:
1820
1921
:ivar message: Message that invoked the slash command.
2022
:ivar name: Name of the command.
23+
:ivar args: List of processed arguments invoked with the command.
24+
:ivar kwargs: Dictionary of processed arguments invoked with the command.
2125
:ivar subcommand_name: Subcommand of the command.
2226
:ivar subcommand_group: Subcommand group of the command.
2327
:ivar interaction_id: Interaction ID of the command message.
2428
:ivar command_id: ID of the command.
25-
:ivar _http: :class:`.http.SlashCommandRequest` of the client.
2629
:ivar bot: discord.py client.
27-
:ivar logger: Logger instance.
28-
:ivar sent: Whether you sent the initial response.
30+
:ivar _http: :class:`.http.SlashCommandRequest` of the client.
31+
:ivar _logger: Logger instance.
32+
:ivar deferred: Whether the command is current deferred (loading state)
33+
:ivar _deferred_hidden: Internal var to check that state stays the same
34+
:ivar responded: Whether you have responded with a message to the interaction.
2935
:ivar guild_id: Guild ID of the command message. If the command was invoked in DM, then it is ``None``
3036
:ivar author_id: User ID representing author of the command message.
3137
:ivar channel_id: Channel ID representing channel of the command message.
@@ -40,14 +46,18 @@ def __init__(self,
4046
self.__token = _json["token"]
4147
self.message = None # Should be set later.
4248
self.name = self.command = self.invoked_with = _json["data"]["name"]
49+
self.args = []
50+
self.kwargs = {}
4351
self.subcommand_name = self.invoked_subcommand = self.subcommand_passed = None
4452
self.subcommand_group = self.invoked_subcommand_group = self.subcommand_group_passed = None
4553
self.interaction_id = _json["id"]
4654
self.command_id = _json["data"]["id"]
4755
self._http = _http
4856
self.bot = _discord
49-
self.logger = logger
50-
self.sent = False
57+
self._logger = logger
58+
self.deferred = False
59+
self.responded = False
60+
self._deferred_hidden = False # To check if the patch to the deferred response matches
5161
self.guild_id = int(_json["guild_id"]) if "guild_id" in _json.keys() else None
5262
self.author_id = int(_json["member"]["user"]["id"] if "member" in _json.keys() else _json["user"]["id"])
5363
self.channel_id = int(_json["channel_id"])
@@ -58,6 +68,26 @@ def __init__(self,
5868
else:
5969
self.author = discord.User(data=_json["user"], state=self.bot._connection)
6070

71+
@property
72+
def _deffered_hidden(self):
73+
warn("`_deffered_hidden` as been renamed to `_deferred_hidden`.", DeprecationWarning, stacklevel=2)
74+
return self._deferred_hidden
75+
76+
@_deffered_hidden.setter
77+
def _deffered_hidden(self, value):
78+
warn("`_deffered_hidden` as been renamed to `_deferred_hidden`.", DeprecationWarning, stacklevel=2)
79+
self._deferred_hidden = value
80+
81+
@property
82+
def deffered(self):
83+
warn("`deffered` as been renamed to `deferred`.", DeprecationWarning, stacklevel=2)
84+
return self.deferred
85+
86+
@deffered.setter
87+
def deffered(self, value):
88+
warn("`deffered` as been renamed to `deferred`.", DeprecationWarning, stacklevel=2)
89+
self.deferred = value
90+
6191
@property
6292
def guild(self) -> typing.Optional[discord.Guild]:
6393
"""
@@ -76,39 +106,20 @@ def channel(self) -> typing.Optional[typing.Union[discord.TextChannel, discord.D
76106
"""
77107
return self.bot.get_channel(self.channel_id)
78108

79-
async def respond(self, eat: bool = False):
109+
async def defer(self, hidden: bool = False):
80110
"""
81-
Sends command invoke response.\n
82-
You should call this first.
83-
84-
.. note::
85-
- If `eat` is ``False``, there is a chance that ``message`` variable is present.
86-
- While it is recommended to be manually called, this will still be automatically called
87-
if this isn't called but :meth:`.send()` is called.
111+
'Defers' the response, showing a loading state to the user
88112
89-
:param eat: Whether to eat user's input. Default ``False``.
113+
:param hidden: Whether the deferred response should be ephemeral . Default ``False``.
90114
"""
91-
base = {"type": 2 if eat else 5}
92-
_task = self.bot.loop.create_task(self._http.post(base, self.interaction_id, self.__token, True))
93-
self.sent = True
94-
if not eat and (not self.guild_id or (self.channel and self.channel.permissions_for(self.guild.me).view_channel)):
95-
with suppress(asyncio.TimeoutError):
96-
def check(message: discord.Message):
97-
user_id = self.author_id
98-
is_author = message.author.id == user_id
99-
channel_id = self.channel_id
100-
is_channel = channel_id == message.channel.id
101-
is_user_input = message.type == 20
102-
is_correct_command = message.content.startswith(f"</{self.name}:{self.command_id}>")
103-
return is_author and is_channel and is_user_input and is_correct_command
104-
105-
self.message = await self.bot.wait_for("message", timeout=3, check=check)
106-
await _task
107-
108-
@property
109-
def ack(self):
110-
"""Alias of :meth:`.respond`."""
111-
return self.respond
115+
if self.deferred or self.responded:
116+
raise error.AlreadyResponded("You have already responded to this command!")
117+
base = {"type": 5}
118+
if hidden:
119+
base["data"] = {"flags": 64}
120+
self._deferred_hidden = True
121+
await self._http.post_initial_response(base, self.interaction_id, self.__token)
122+
self.deferred = True
112123

113124
async def send(self,
114125
content: str = "", *,
@@ -129,6 +140,7 @@ async def send(self,
129140
.. warning::
130141
- Since Release 1.0.9, this is completely changed. If you are migrating from older version, please make sure to fix the usage.
131142
- You can't use both ``embed`` and ``embeds`` at the same time, also applies to ``file`` and ``files``.
143+
- You cannot send files in the initial response
132144
133145
:param content: Content of the response.
134146
:type content: str
@@ -150,15 +162,6 @@ async def send(self,
150162
:type delete_after: float
151163
:return: Union[discord.Message, dict]
152164
"""
153-
if isinstance(content, int) and 2 <= content <= 5:
154-
raise error.IncorrectFormat("`.send` Method is rewritten at Release 1.0.9. Please read the docs and fix all the usages.")
155-
if not self.sent:
156-
self.logger.info(f"At command `{self.name}`: It is recommended to call `.respond()` first!")
157-
await self.respond(eat=hidden)
158-
if hidden:
159-
if embeds or embed or files or file:
160-
self.logger.warning("Embed/File is not supported for `hidden`!")
161-
return await self.send_hidden(content)
162165
if embed and embeds:
163166
raise error.IncorrectFormat("You can't use both `embed` and `embeds`!")
164167
if embed:
@@ -172,6 +175,8 @@ async def send(self,
172175
raise error.IncorrectFormat("You can't use both `file` and `files`!")
173176
if file:
174177
files = [file]
178+
if delete_after and hidden:
179+
raise error.IncorrectFormat("You can't delete a hidden message!")
175180

176181
base = {
177182
"content": content,
@@ -180,30 +185,47 @@ async def send(self,
180185
"allowed_mentions": allowed_mentions.to_dict() if allowed_mentions
181186
else self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {}
182187
}
183-
184-
resp = await self._http.post(base, self.interaction_id, self.__token, files=files)
185-
smsg = model.SlashMessage(state=self.bot._connection,
186-
data=resp,
187-
channel=self.channel or discord.Object(id=self.channel_id),
188-
_http=self._http,
189-
interaction_token=self.__token)
190-
if delete_after:
191-
self.bot.loop.create_task(smsg.delete(delay=delete_after))
192-
return smsg
193-
194-
def send_hidden(self, content: str = ""):
195-
"""
196-
Sends hidden response.\n
197-
This is automatically used if you pass ``hidden=True`` at :meth:`.send`.
198-
199-
.. note::
200-
This is not intended to be manually called. Please use :meth:`.send` instead.
201-
202-
:param content: Message content.
203-
:return: Coroutine
204-
"""
205-
base = {
206-
"content": content,
207-
"flags": 64
208-
}
209-
return self._http.post(base, self.interaction_id, self.__token)
188+
if hidden:
189+
if embeds or files:
190+
self._logger.warning("Embed/File is not supported for `hidden`!")
191+
base["flags"] = 64
192+
193+
initial_message = False
194+
if not self.responded:
195+
initial_message = True
196+
if files:
197+
raise error.IncorrectFormat("You cannot send files in the initial response!")
198+
if self.deferred:
199+
if self._deferred_hidden != hidden:
200+
self._logger.warning(
201+
"deferred response might not be what you set it to! (hidden / visible) "
202+
"This is because it was deferred in a different state"
203+
)
204+
resp = await self._http.edit(base, self.__token)
205+
self.deferred = False
206+
else:
207+
json_data = {
208+
"type": 4,
209+
"data": base
210+
}
211+
await self._http.post_initial_response(json_data, self.interaction_id, self.__token)
212+
if not hidden:
213+
resp = await self._http.edit({}, self.__token)
214+
else:
215+
resp = {}
216+
self.responded = True
217+
else:
218+
resp = await self._http.post_followup(base, self.__token, files=files)
219+
if not hidden:
220+
smsg = model.SlashMessage(state=self.bot._connection,
221+
data=resp,
222+
channel=self.channel or discord.Object(id=self.channel_id),
223+
_http=self._http,
224+
interaction_token=self.__token)
225+
if delete_after:
226+
self.bot.loop.create_task(smsg.delete(delay=delete_after))
227+
if initial_message:
228+
self.message = smsg
229+
return smsg
230+
else:
231+
return resp

discord_slash/error.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,14 @@ class IncorrectType(SlashCommandError):
5555
Type passed was incorrect
5656
"""
5757

58+
5859
class IncorrectCommandData(SlashCommandError):
5960
"""
6061
Incorrect data was passed to a slash command data object
6162
"""
63+
64+
65+
class AlreadyResponded(SlashCommandError):
66+
"""
67+
The interaction was already responded to
68+
"""

discord_slash/http.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import aiohttp
44
import discord
55
from discord.http import Route
6+
from . import error
67

78

89
class CustomRoute(Route):
@@ -83,49 +84,75 @@ def command_request(self, method, guild_id, url_ending="", **kwargs):
8384
route = CustomRoute(method, url)
8485
return self._discord.http.request(route, **kwargs)
8586

86-
def post(self, _resp, interaction_id, token, initial=False, files: typing.List[discord.File] = None):
87+
def post_followup(self, _resp, token, files: typing.List[discord.File] = None):
8788
"""
88-
Sends command response POST request to Discord API.
89+
Sends command followup response POST request to Discord API.
8990
9091
:param _resp: Command response.
9192
:type _resp: dict
92-
:param interaction_id: Interaction ID.
9393
:param token: Command message token.
94-
:param initial: Whether this request is initial. Default ``False``
9594
:param files: Files to send. Default ``None``
9695
:type files: List[discord.File]
9796
:return: Coroutine
9897
"""
9998
if files:
10099
return self.post_with_files(_resp, files, token)
101-
req_url = f"/interactions/{interaction_id}/{token}/callback" if initial else f"/webhooks/{self.application_id}/{token}"
102-
route = CustomRoute("POST", req_url)
103-
return self._discord.http.request(route, json=_resp)
100+
return self.command_response(token, True, "POST", json=_resp)
101+
102+
def post_initial_response(self, _resp, interaction_id, token):
103+
"""
104+
Sends an initial "POST" response to the Discord API.
105+
106+
:param _resp: Command response.
107+
:type _resp: dict
108+
:param interaction_id: Interaction ID.
109+
:param token: Command message token.
110+
:return: Coroutine
111+
"""
112+
return self.command_response(token, False, "POST", interaction_id, json=_resp)
113+
114+
def command_response(self, token, use_webhook, method, interaction_id= None, url_ending = "", **kwargs):
115+
"""
116+
Sends a command response to discord (POST, PATCH, DELETE)
117+
118+
:param token: Interaction token
119+
:param use_webhook: Whether to use webhooks
120+
:param method: The HTTP request to use
121+
:param interaction_id: The id of the interaction
122+
:param url_ending: String to append onto the end of the url.
123+
:param \**kwargs: Kwargs to pass into discord.py's `request function <https://github.com/Rapptz/discord.py/blob/master/discord/http.py#L134>`_
124+
:return: Coroutine
125+
"""
126+
if not use_webhook and not interaction_id:
127+
raise error.IncorrectFormat("Internal Error! interaction_id must be set if use_webhook is False")
128+
req_url = f"/webhooks/{self.application_id}/{token}" if use_webhook else f"/interactions/{interaction_id}/{token}/callback"
129+
req_url += url_ending
130+
route = CustomRoute(method, req_url)
131+
return self._discord.http.request(route, **kwargs)
104132

105133
def post_with_files(self, _resp, files: typing.List[discord.File], token):
106-
req_url = f"/webhooks/{self.application_id}/{token}"
107-
route = CustomRoute("POST", req_url)
134+
108135
form = aiohttp.FormData()
109136
form.add_field("payload_json", json.dumps(_resp))
110137
for x in range(len(files)):
111138
name = f"file{x if len(files) > 1 else ''}"
112139
sel = files[x]
113140
form.add_field(name, sel.fp, filename=sel.filename, content_type="application/octet-stream")
114-
return self._discord.http.request(route, data=form, files=files)
141+
return self.command_response(token, True, "POST", data=form, files=files)
115142

116143
def edit(self, _resp, token, message_id="@original"):
117144
"""
118-
Sends edit command response POST request to Discord API.
145+
Sends edit command response PATCH request to Discord API.
119146
120147
:param _resp: Edited response.
121148
:type _resp: dict
122149
:param token: Command message token.
123150
:param message_id: Message ID to edit. Default initial message.
124151
:return: Coroutine
125152
"""
126-
req_url = f"/webhooks/{self.application_id}/{token}/messages/{message_id}"
127-
route = CustomRoute("PATCH", req_url)
128-
return self._discord.http.request(route, json=_resp)
153+
req_url = f"/messages/{message_id}"
154+
return self.command_response(token, True, "PATCH", url_ending = req_url, json=_resp)
155+
129156

130157
def delete(self, token, message_id="@original"):
131158
"""
@@ -135,6 +162,6 @@ def delete(self, token, message_id="@original"):
135162
:param message_id: Message ID to delete. Default initial message.
136163
:return: Coroutine
137164
"""
138-
req_url = f"/webhooks/{self.application_id}/{token}/messages/{message_id}"
139-
route = CustomRoute("DELETE", req_url)
140-
return self._discord.http.request(route)
165+
req_url = f"/messages/{message_id}"
166+
return self.command_response(token, True, "DELETE", url_ending = req_url)
167+

0 commit comments

Comments
 (0)