Skip to content

Commit 4443bb0

Browse files
authored
Merge pull request #107 from AnotherCat/new-ui-changes
Change context and http to reflect upcoming changes
2 parents 4d974f5 + 1dced2b commit 4443bb0

File tree

8 files changed

+181
-98
lines changed

8 files changed

+181
-98
lines changed

discord_slash/context.py

Lines changed: 67 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ class SlashContext:
2222
:ivar subcommand_group: Subcommand group of the command.
2323
:ivar interaction_id: Interaction ID of the command message.
2424
:ivar command_id: ID of the command.
25-
:ivar _http: :class:`.http.SlashCommandRequest` of the client.
2625
:ivar bot: discord.py client.
27-
:ivar logger: Logger instance.
28-
:ivar sent: Whether you sent the initial response.
26+
:ivar _http: :class:`.http.SlashCommandRequest` of the client.
27+
:ivar _logger: Logger instance.
28+
:ivar deffered: Whether the command is current deffered (loading state)
29+
:ivar _deffered_hidden: Internal var to check that state stays the same
30+
:ivar responded: Whether you have responded with a message to the interaction.
2931
:ivar guild_id: Guild ID of the command message. If the command was invoked in DM, then it is ``None``
3032
:ivar author_id: User ID representing author of the command message.
3133
:ivar channel_id: Channel ID representing channel of the command message.
@@ -46,8 +48,10 @@ def __init__(self,
4648
self.command_id = _json["data"]["id"]
4749
self._http = _http
4850
self.bot = _discord
49-
self.logger = logger
50-
self.sent = False
51+
self._logger = logger
52+
self.deffered = False
53+
self.responded = False
54+
self._deffered_hidden = False # To check if the patch to the deffered response matches
5155
self.guild_id = int(_json["guild_id"]) if "guild_id" in _json.keys() else None
5256
self.author_id = int(_json["member"]["user"]["id"] if "member" in _json.keys() else _json["user"]["id"])
5357
self.channel_id = int(_json["channel_id"])
@@ -76,39 +80,20 @@ def channel(self) -> typing.Optional[typing.Union[discord.TextChannel, discord.D
7680
"""
7781
return self.bot.get_channel(self.channel_id)
7882

79-
async def respond(self, eat: bool = False):
83+
async def defer(self, hidden: bool = False):
8084
"""
81-
Sends command invoke response.\n
82-
You should call this first.
85+
'Deferes' the response, showing a loading state to the user
8386
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.
88-
89-
:param eat: Whether to eat user's input. Default ``False``.
87+
:param hidden: Whether the deffered response should be ephemeral . Default ``False``.
9088
"""
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
89+
if self.deffered or self.responded:
90+
raise error.AlreadyResponded("You have already responded to this command!")
91+
base = {"type": 5}
92+
if hidden:
93+
base["data"] = {"flags": 64}
94+
self._deffered_hidden = True
95+
await self._http.post_initial_response(base, self.interaction_id, self.__token)
96+
self.deffered = True
11297

11398
async def send(self,
11499
content: str = "", *,
@@ -129,6 +114,7 @@ async def send(self,
129114
.. warning::
130115
- Since Release 1.0.9, this is completely changed. If you are migrating from older version, please make sure to fix the usage.
131116
- You can't use both ``embed`` and ``embeds`` at the same time, also applies to ``file`` and ``files``.
117+
- You cannot send files in the initial response
132118
133119
:param content: Content of the response.
134120
:type content: str
@@ -150,15 +136,6 @@ async def send(self,
150136
:type delete_after: float
151137
:return: Union[discord.Message, dict]
152138
"""
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)
162139
if embed and embeds:
163140
raise error.IncorrectFormat("You can't use both `embed` and `embeds`!")
164141
if embed:
@@ -172,6 +149,8 @@ async def send(self,
172149
raise error.IncorrectFormat("You can't use both `file` and `files`!")
173150
if file:
174151
files = [file]
152+
if delete_after and hidden:
153+
raise error.IncorrectFormat("You can't delete a hidden message!")
175154

176155
base = {
177156
"content": content,
@@ -180,30 +159,47 @@ async def send(self,
180159
"allowed_mentions": allowed_mentions.to_dict() if allowed_mentions
181160
else self.bot.allowed_mentions.to_dict() if self.bot.allowed_mentions else {}
182161
}
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)
162+
if hidden:
163+
if embeds or files:
164+
self._logger.warning("Embed/File is not supported for `hidden`!")
165+
base["flags"] = 64
166+
167+
initial_message = False
168+
if not self.responded:
169+
initial_message = True
170+
if files:
171+
raise error.IncorrectFormat("You cannot send files in the initial response!")
172+
if self.deffered:
173+
if self._deffered_hidden != hidden:
174+
self._logger.warning(
175+
"Deffered response might not be what you set it to! (hidden / visible) "
176+
"This is because it was deffered in a different state"
177+
)
178+
resp = await self._http.edit(base, self.__token)
179+
self.deffered = False
180+
else:
181+
json_data = {
182+
"type": 4,
183+
"data": base
184+
}
185+
await self._http.post_initial_response(json_data, self.interaction_id, self.__token)
186+
if not hidden:
187+
resp = await self._http.edit({}, self.__token)
188+
else:
189+
resp = {}
190+
self.responded = True
191+
else:
192+
resp = await self._http.post_followup(base, self.__token, files=files)
193+
if not hidden:
194+
smsg = model.SlashMessage(state=self.bot._connection,
195+
data=resp,
196+
channel=self.channel or discord.Object(id=self.channel_id),
197+
_http=self._http,
198+
interaction_token=self.__token)
199+
if delete_after:
200+
self.bot.loop.create_task(smsg.delete(delay=delete_after))
201+
if initial_message:
202+
self.message = smsg
203+
return smsg
204+
else:
205+
return resp

discord_slash/error.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,8 @@ class IncorrectType(SlashCommandError):
5454
"""
5555
Type passed was incorrect
5656
"""
57+
58+
class AlreadyResponded(SlashCommandError):
59+
"""
60+
The interaction was already responded to
61+
"""

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+

docs/faq.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Pretty much anything from the discord's commands extension doesn't work, also so
109109
.. warning::
110110
If you use something that might take a while, eg ``wait_for`` you'll run into two issues:
111111

112-
1. If you don't respond within 3 seconds (``ctx.respond()``) discord invalidates the interaction.
112+
1. If you don't respond within 3 seconds (``ctx.defer()`` or `ctx.send(..)``) discord invalidates the interaction.
113113
2. The interaction only lasts for 15 minutes, so if you try and send something with the interaction (``ctx.send``) more than 15 mins after the command was ran it won't work.
114114

115115
As an alternative you can use ``ctx.channel.send`` but this relies on the the bot being in the guild, and the bot having send perms in that channel.

docs/gettingstarted.rst

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,12 @@ code as shown below:
5454
@slash.slash(name="test",
5555
description="This is just a test command, nothing more.")
5656
async def test(ctx):
57-
await ctx.respond()
5857
await ctx.send(content="Hello World!")
5958
6059
Now that we've gone over how Discord handles the declaration of slash commands
6160
through their Bot API, let's go over what some of the other things mean within
6261
the *logical* part of our code, the command function:
6362

64-
- ``ctx.respond()``: This is a way for us to handle responses. In short, the API
65-
requires some way to "acknowledge" an interaction response that we want to send off.
66-
67-
(An alias of this would be ``ctx.ack()``)
6863

6964
Giving some options for variety.
7065
--------------------------------
@@ -163,7 +158,6 @@ Now, we can finally visualize this by coding an example of this being used in th
163158
)
164159
])
165160
async def test(ctx, optone: str):
166-
await ctx.respond()
167161
await ctx.send(content=f"I got you, you said {optone}!")
168162
169163
Additionally, we could also declare the type of our command's option through this method shown here:
@@ -233,7 +227,6 @@ a string or integer. Below is an implementation of this design in the Python cod
233227
)
234228
])
235229
async def test(ctx, optone: str):
236-
await ctx.respond()
237230
await ctx.send(content=f"Wow, you actually chose {optone}? :(")
238231
239232
.. _quickstart: https://discord-py-slash-command.readthedocs.io/en/latest/quickstart.html

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ discussion on GitHub!
2626

2727
quickstart.rst
2828
gettingstarted.rst
29-
migrate_to_109.rst
29+
migration.rst
3030
discord_slash.rst
3131
events.rst
3232
discord_slash.utils.rst

0 commit comments

Comments
 (0)