1+ import datetime
12import typing
23import asyncio
34from warnings import warn
45
56import discord
67from contextlib import suppress
78from discord .ext import commands
9+ from discord .utils import snowflake_time
10+
811from . import http
912from . import error
1013from . import model
14+ from . dpy_overrides import ComponentMessage
1115
1216
13- class SlashContext :
17+ class InteractionContext :
1418 """
15- Context of the slash command .\n
19+ Base context for interactions .\n
1620 Kinda similar with discord.ext.commands.Context.
1721
1822 .. warning::
1923 Do not manually init this model.
2024
2125 :ivar message: Message that invoked the slash command.
22- :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.
25- :ivar subcommand_name: Subcommand of the command.
26- :ivar subcommand_group: Subcommand group of the command.
2726 :ivar interaction_id: Interaction ID of the command message.
28- :ivar command_id: ID of the command.
2927 :ivar bot: discord.py client.
3028 :ivar _http: :class:`.http.SlashCommandRequest` of the client.
3129 :ivar _logger: Logger instance.
@@ -43,15 +41,9 @@ def __init__(self,
4341 _json : dict ,
4442 _discord : typing .Union [discord .Client , commands .Bot ],
4543 logger ):
46- self .__token = _json ["token" ]
44+ self ._token = _json ["token" ]
4745 self .message = None # Should be set later.
48- self .name = self .command = self .invoked_with = _json ["data" ]["name" ]
49- self .args = []
50- self .kwargs = {}
51- self .subcommand_name = self .invoked_subcommand = self .subcommand_passed = None
52- self .subcommand_group = self .invoked_subcommand_group = self .subcommand_group_passed = None
5346 self .interaction_id = _json ["id" ]
54- self .command_id = _json ["data" ]["id" ]
5547 self ._http = _http
5648 self .bot = _discord
5749 self ._logger = logger
@@ -67,6 +59,7 @@ def __init__(self,
6759 self .author = discord .User (data = _json ["member" ]["user" ], state = self .bot ._connection )
6860 else :
6961 self .author = discord .User (data = _json ["user" ], state = self .bot ._connection )
62+ self .created_at : datetime .datetime = snowflake_time (int (self .interaction_id ))
7063
7164 @property
7265 def _deffered_hidden (self ):
@@ -118,7 +111,7 @@ async def defer(self, hidden: bool = False):
118111 if hidden :
119112 base ["data" ] = {"flags" : 64 }
120113 self ._deferred_hidden = True
121- await self ._http .post_initial_response (base , self .interaction_id , self .__token )
114+ await self ._http .post_initial_response (base , self .interaction_id , self ._token )
122115 self .deferred = True
123116
124117 async def send (self ,
@@ -130,7 +123,9 @@ async def send(self,
130123 files : typing .List [discord .File ] = None ,
131124 allowed_mentions : discord .AllowedMentions = None ,
132125 hidden : bool = False ,
133- delete_after : float = None ) -> model .SlashMessage :
126+ delete_after : float = None ,
127+ components : typing .List [dict ] = None ,
128+ ) -> model .SlashMessage :
134129 """
135130 Sends response of the slash command.
136131
@@ -157,6 +152,8 @@ async def send(self,
157152 :type hidden: bool
158153 :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.
159154 :type delete_after: float
155+ :param components: Message components in the response. The top level must be made of ActionRows.
156+ :type components: List[dict]
160157 :return: Union[discord.Message, dict]
161158 """
162159 if embed and embeds :
@@ -174,13 +171,16 @@ async def send(self,
174171 files = [file ]
175172 if delete_after and hidden :
176173 raise error .IncorrectFormat ("You can't delete a hidden message!" )
174+ if components and not all (comp .get ("type" ) == 1 for comp in components ):
175+ raise error .IncorrectFormat ("The top level of the components list must be made of ActionRows!" )
177176
178177 base = {
179178 "content" : content ,
180179 "tts" : tts ,
181180 "embeds" : [x .to_dict () for x in embeds ] if embeds else [],
182181 "allowed_mentions" : allowed_mentions .to_dict () if allowed_mentions
183- else self .bot .allowed_mentions .to_dict () if self .bot .allowed_mentions else {}
182+ else self .bot .allowed_mentions .to_dict () if self .bot .allowed_mentions else {},
183+ "components" : components or [],
184184 }
185185 if hidden :
186186 base ["flags" ] = 64
@@ -196,21 +196,21 @@ async def send(self,
196196 "Deferred response might not be what you set it to! (hidden / visible) "
197197 "This is because it was deferred in a different state."
198198 )
199- resp = await self ._http .edit (base , self .__token , files = files )
199+ resp = await self ._http .edit (base , self ._token , files = files )
200200 self .deferred = False
201201 else :
202202 json_data = {
203203 "type" : 4 ,
204204 "data" : base
205205 }
206- await self ._http .post_initial_response (json_data , self .interaction_id , self .__token )
206+ await self ._http .post_initial_response (json_data , self .interaction_id , self ._token )
207207 if not hidden :
208- resp = await self ._http .edit ({}, self .__token )
208+ resp = await self ._http .edit ({}, self ._token )
209209 else :
210210 resp = {}
211211 self .responded = True
212212 else :
213- resp = await self ._http .post_followup (base , self .__token , files = files )
213+ resp = await self ._http .post_followup (base , self ._token , files = files )
214214 if files :
215215 for file in files :
216216 file .close ()
@@ -219,11 +219,141 @@ async def send(self,
219219 data = resp ,
220220 channel = self .channel or discord .Object (id = self .channel_id ),
221221 _http = self ._http ,
222- interaction_token = self .__token )
222+ interaction_token = self ._token )
223223 if delete_after :
224224 self .bot .loop .create_task (smsg .delete (delay = delete_after ))
225225 if initial_message :
226226 self .message = smsg
227227 return smsg
228228 else :
229229 return resp
230+
231+
232+ class SlashContext (InteractionContext ):
233+ """
234+ Context of a slash command. Has all attributes from :class:`InteractionContext`, plus the slash-command-specific ones below.
235+
236+ :ivar name: Name of the command.
237+ :ivar args: List of processed arguments invoked with the command.
238+ :ivar kwargs: Dictionary of processed arguments invoked with the command.
239+ :ivar subcommand_name: Subcommand of the command.
240+ :ivar subcommand_group: Subcommand group of the command.
241+ :ivar command_id: ID of the command.
242+ """
243+ def __init__ (self ,
244+ _http : http .SlashCommandRequest ,
245+ _json : dict ,
246+ _discord : typing .Union [discord .Client , commands .Bot ],
247+ logger ):
248+ self .name = self .command = self .invoked_with = _json ["data" ]["name" ]
249+ self .args = []
250+ self .kwargs = {}
251+ self .subcommand_name = self .invoked_subcommand = self .subcommand_passed = None
252+ self .subcommand_group = self .invoked_subcommand_group = self .subcommand_group_passed = None
253+ self .command_id = _json ["data" ]["id" ]
254+
255+ super ().__init__ (_http = _http , _json = _json , _discord = _discord , logger = logger )
256+
257+
258+ class ComponentContext (InteractionContext ):
259+ """
260+ Context of a component interaction. Has all attributes from :class:`InteractionContext`, plus the component-specific ones below.
261+
262+ :ivar custom_id: The custom ID of the component.
263+ :ivar component_type: The type of the component.
264+ :ivar origin_message: The origin message of the component. Not available if the origin message was ephemeral.
265+ :ivar origin_message_id: The ID of the origin message.
266+ """
267+ def __init__ (self ,
268+ _http : http .SlashCommandRequest ,
269+ _json : dict ,
270+ _discord : typing .Union [discord .Client , commands .Bot ],
271+ logger ):
272+ self .custom_id = self .component_id = _json ["data" ]["custom_id" ]
273+ self .component_type = _json ["data" ]["component_type" ]
274+ super ().__init__ (_http = _http , _json = _json , _discord = _discord , logger = logger )
275+ self .origin_message = None
276+ self .origin_message_id = int (_json ["message" ]["id" ]) if "message" in _json .keys () else None
277+
278+ if self .origin_message_id and (_json ["message" ]["flags" ] & 64 ) != 64 :
279+ self .origin_message = ComponentMessage (state = self .bot ._connection , channel = self .channel ,
280+ data = _json ["message" ])
281+
282+ async def defer (self , hidden : bool = False , edit_origin : bool = False ):
283+ """
284+ 'Defers' the response, showing a loading state to the user
285+
286+ :param hidden: Whether the deferred response should be ephemeral . Default ``False``.
287+ :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``.
288+ """
289+ if self .deferred or self .responded :
290+ raise error .AlreadyResponded ("You have already responded to this command!" )
291+ base = {"type" : 6 if edit_origin else 5 }
292+ if hidden and not edit_origin :
293+ base ["data" ] = {"flags" : 64 }
294+ self ._deferred_hidden = True
295+ await self ._http .post_initial_response (base , self .interaction_id , self ._token )
296+ self .deferred = True
297+
298+ async def edit_origin (self , ** fields ):
299+ """
300+ Edits the origin message of the component.
301+ Refer to :meth:`discord.Message.edit` and :meth:`InteractionContext.send` for fields.
302+ """
303+ _resp = {}
304+
305+ content = fields .get ("content" )
306+ if content :
307+ _resp ["content" ] = str (content )
308+
309+ embed = fields .get ("embed" )
310+ embeds = fields .get ("embeds" )
311+ file = fields .get ("file" )
312+ files = fields .get ("files" )
313+ components = fields .get ("components" )
314+
315+ if components :
316+ _resp ["components" ] = components
317+
318+ if embed and embeds :
319+ raise error .IncorrectFormat ("You can't use both `embed` and `embeds`!" )
320+ if file and files :
321+ raise error .IncorrectFormat ("You can't use both `file` and `files`!" )
322+ if file :
323+ files = [file ]
324+ if embed :
325+ embeds = [embed ]
326+ if embeds :
327+ if not isinstance (embeds , list ):
328+ raise error .IncorrectFormat ("Provide a list of embeds." )
329+ elif len (embeds ) > 10 :
330+ raise error .IncorrectFormat ("Do not provide more than 10 embeds." )
331+ _resp ["embeds" ] = [x .to_dict () for x in embeds ]
332+
333+ allowed_mentions = fields .get ("allowed_mentions" )
334+ _resp ["allowed_mentions" ] = allowed_mentions .to_dict () if allowed_mentions else \
335+ self .bot .allowed_mentions .to_dict () if self .bot .allowed_mentions else {}
336+
337+ if not self .responded :
338+ if files and not self .deferred :
339+ await self .defer (edit_origin = True )
340+ if self .deferred :
341+ _json = await self ._http .edit (_resp , self ._token , files = files )
342+ self .deferred = False
343+ else :
344+ json_data = {
345+ "type" : 7 ,
346+ "data" : _resp
347+ }
348+ _json = await self ._http .post_initial_response (json_data , self .interaction_id , self ._token )
349+ self .responded = True
350+ else :
351+ raise error .IncorrectFormat ("Already responded" )
352+
353+ if files :
354+ for file in files :
355+ file .close ()
356+
357+ # Commented out for now as sometimes (or at least, when not deferred) _json is an empty string?
358+ # self.origin_message = ComponentMessage(state=self.bot._connection, channel=self.channel,
359+ # data=_json)
0 commit comments