@@ -46,6 +46,7 @@ class SlashCommand:
46
46
47
47
:ivar _discord: Discord client of this client.
48
48
:ivar commands: Dictionary of the registered commands via :func:`.slash` decorator.
49
+ :ivar menu_commands: Dictionary of the registered context menus via the :func:`.context_menu` decorator.
49
50
:ivar req: :class:`.http.SlashCommandRequest` of this client.
50
51
:ivar logger: Logger of this client.
51
52
:ivar sync_commands: Whether to sync commands automatically.
@@ -64,7 +65,7 @@ def __init__(
64
65
application_id : typing .Optional [int ] = None ,
65
66
):
66
67
self ._discord = client
67
- self .commands = {}
68
+ self .commands = {"context" : {} }
68
69
self .subcommands = {}
69
70
self .components = {}
70
71
self .logger = logging .getLogger ("discord_slash" )
@@ -270,12 +271,53 @@ async def to_dict(self):
270
271
await self ._discord .wait_until_ready () # In case commands are still not registered to SlashCommand.
271
272
all_guild_ids = []
272
273
for x in self .commands :
274
+ if x == "context" :
275
+ # handle context menu separately.
276
+ for _x in self .commands ["context" ]:
277
+ _selected = self .commands ["context" ][_x ]
278
+ for i in _selected .allowed_guild_ids :
279
+ if i not in all_guild_ids :
280
+ all_guild_ids .append (i )
281
+ continue
273
282
for i in self .commands [x ].allowed_guild_ids :
274
283
if i not in all_guild_ids :
275
284
all_guild_ids .append (i )
276
285
cmds = {"global" : [], "guild" : {x : [] for x in all_guild_ids }}
277
286
wait = {} # Before merging to return dict, let's first put commands to temporary dict.
278
287
for x in self .commands :
288
+ if x == "context" :
289
+ # handle context menu separately.
290
+ for _x in self .commands ["context" ]: # x is the new reference dict
291
+ selected = self .commands ["context" ][_x ]
292
+
293
+ if selected .allowed_guild_ids :
294
+ for y in selected .allowed_guild_ids :
295
+ if y not in wait :
296
+ wait [y ] = {}
297
+ command_dict = {
298
+ "name" : _x ,
299
+ "options" : selected .options or [],
300
+ "default_permission" : selected .default_permission ,
301
+ "permissions" : {},
302
+ "type" : selected ._type ,
303
+ }
304
+ if y in selected .permissions :
305
+ command_dict ["permissions" ][y ] = selected .permissions [y ]
306
+ wait [y ][x ] = copy .deepcopy (command_dict )
307
+ else :
308
+ if "global" not in wait :
309
+ wait ["global" ] = {}
310
+ command_dict = {
311
+ "name" : _x ,
312
+ "options" : selected .options or [],
313
+ "default_permission" : selected .default_permission ,
314
+ "permissions" : selected .permissions or {},
315
+ "type" : selected ._type ,
316
+ }
317
+ wait ["global" ][x ] = copy .deepcopy (command_dict )
318
+
319
+ continue
320
+
279
321
selected = self .commands [x ]
280
322
if selected .allowed_guild_ids :
281
323
for y in selected .allowed_guild_ids :
@@ -287,7 +329,10 @@ async def to_dict(self):
287
329
"options" : selected .options or [],
288
330
"default_permission" : selected .default_permission ,
289
331
"permissions" : {},
332
+ "type" : selected ._type ,
290
333
}
334
+ if command_dict ["type" ] != 1 :
335
+ command_dict .pop ("description" )
291
336
if y in selected .permissions :
292
337
command_dict ["permissions" ][y ] = selected .permissions [y ]
293
338
wait [y ][x ] = copy .deepcopy (command_dict )
@@ -300,14 +345,20 @@ async def to_dict(self):
300
345
"options" : selected .options or [],
301
346
"default_permission" : selected .default_permission ,
302
347
"permissions" : selected .permissions or {},
348
+ "type" : selected ._type ,
303
349
}
350
+ if command_dict ["type" ] != 1 :
351
+ command_dict .pop ("description" )
304
352
wait ["global" ][x ] = copy .deepcopy (command_dict )
305
353
306
354
# Separated normal command add and subcommand add not to
307
355
# merge subcommands to one. More info at Issue #88
308
356
# https://github.com/eunwoo1104/discord-py-slash-command/issues/88
309
357
310
358
for x in self .commands :
359
+ if x == "context" :
360
+ continue # no menus have subcommands.
361
+
311
362
if not self .commands [x ].has_subcommands :
312
363
continue
313
364
tgt = self .subcommands [x ]
@@ -424,7 +475,7 @@ async def sync_all_commands(
424
475
if ex .status == 400 :
425
476
# catch bad requests
426
477
cmd_nums = set (
427
- re .findall (r"In\s(\d). " , ex .args [0 ])
478
+ re .findall (r"^[\w-]{1,32}$ " , ex .args [0 ])
428
479
) # find all discords references to commands
429
480
error_string = ex .args [0 ]
430
481
@@ -594,6 +645,66 @@ def add_slash_command(
594
645
self .logger .debug (f"Added command `{ name } `" )
595
646
return obj
596
647
648
+ def _cog_ext_add_context_menu (self , target : int , name : str , guild_ids : list = None ):
649
+ """
650
+ Creates a new cog_based context menu command.
651
+
652
+ :param cmd: Command Coroutine.
653
+ :type cmd: Coroutine
654
+ :param name: The name of the command
655
+ :type name: str
656
+ :param _type: The context menu type.
657
+ :type _type: int
658
+ """
659
+
660
+ def add_context_menu (self , cmd , name : str , _type : int , guild_ids : list = None ):
661
+ """
662
+ Creates a new context menu command.
663
+
664
+ :param cmd: Command Coroutine.
665
+ :type cmd: Coroutine
666
+ :param name: The name of the command
667
+ :type name: str
668
+ :param _type: The context menu type.
669
+ :type _type: int
670
+ """
671
+
672
+ name = [name or cmd .__name__ ][0 ]
673
+ guild_ids = guild_ids or []
674
+
675
+ if not all (isinstance (item , int ) for item in guild_ids ):
676
+ raise error .IncorrectGuildIDType (
677
+ f"The snowflake IDs { guild_ids } given are not a list of integers. Because of discord.py convention, please use integer IDs instead. Furthermore, the command '{ name } ' will be deactivated and broken until fixed."
678
+ )
679
+
680
+ if name in self .commands ["context" ]:
681
+ tgt = self .commands ["context" ][name ]
682
+ if not tgt .has_subcommands :
683
+ raise error .DuplicateCommand (name )
684
+ has_subcommands = tgt .has_subcommands # noqa
685
+ for x in tgt .allowed_guild_ids :
686
+ if x not in guild_ids :
687
+ guild_ids .append (x )
688
+
689
+ _cmd = {
690
+ "default_permission" : None ,
691
+ "has_permissions" : None ,
692
+ "name" : name ,
693
+ "type" : _type ,
694
+ "func" : cmd ,
695
+ "description" : "" ,
696
+ "guild_ids" : guild_ids ,
697
+ "api_options" : [],
698
+ "connector" : {},
699
+ "has_subcommands" : False ,
700
+ "api_permissions" : {},
701
+ }
702
+
703
+ obj = model .BaseCommandObject (name , cmd = _cmd , _type = _type )
704
+ self .commands ["context" ][name ] = obj
705
+ self .logger .debug (f"Added context command `{ name } `" )
706
+ return obj
707
+
597
708
def add_subcommand (
598
709
self ,
599
710
cmd ,
@@ -916,6 +1027,34 @@ def wrapper(cmd):
916
1027
917
1028
return wrapper
918
1029
1030
+ def context_menu (self , * , target : int , name : str , guild_ids : list = None ):
1031
+ """
1032
+ Decorator that adds context menu commands.
1033
+
1034
+ :param target: The type of menu.
1035
+ :type target: int
1036
+ :param name: A name to register as the command in the menu.
1037
+ :type name: str
1038
+ :param guild_ids: A list of guild IDs to register the command under. Defaults to ``None``.
1039
+ :type guild_ids: list
1040
+ """
1041
+
1042
+ def wrapper (cmd ):
1043
+ # _obj = self.add_slash_command(
1044
+ # cmd,
1045
+ # name,
1046
+ # "",
1047
+ # guild_ids
1048
+ # )
1049
+
1050
+ # This has to call both, as its a arg-less menu.
1051
+
1052
+ obj = self .add_context_menu (cmd , name , target , guild_ids )
1053
+
1054
+ return obj
1055
+
1056
+ return wrapper
1057
+
919
1058
def add_component_callback (
920
1059
self ,
921
1060
callback : typing .Coroutine ,
@@ -1255,12 +1394,15 @@ async def on_socket_response(self, msg):
1255
1394
1256
1395
to_use = msg ["d" ]
1257
1396
interaction_type = to_use ["type" ]
1258
- if interaction_type in (1 , 2 ):
1259
- return await self ._on_slash (to_use )
1260
- if interaction_type == 3 :
1261
- return await self ._on_component (to_use )
1262
-
1263
- raise NotImplementedError
1397
+ if interaction_type in (1 , 2 , 3 ) or msg ["s" ] == 5 :
1398
+ await self ._on_slash (to_use )
1399
+ await self ._on_context_menu (to_use )
1400
+ try :
1401
+ await self ._on_component (to_use ) # noqa
1402
+ except KeyError :
1403
+ pass # for some reason it complains about custom_id being an optional arg when it's fine?
1404
+ return
1405
+ # raise NotImplementedError
1264
1406
1265
1407
async def _on_component (self , to_use ):
1266
1408
ctx = context .ComponentContext (self .req , to_use , self ._discord , self .logger )
@@ -1319,6 +1461,34 @@ async def _on_slash(self, to_use):
1319
1461
1320
1462
await self .invoke_command (selected_cmd , ctx , args )
1321
1463
1464
+ async def _on_context_menu (self , to_use ):
1465
+ if to_use ["data" ]["name" ] in self .commands ["context" ]:
1466
+ ctx = context .MenuContext (self .req , to_use , self ._discord , self .logger )
1467
+ cmd_name = to_use ["data" ]["name" ]
1468
+
1469
+ if cmd_name not in self .commands ["context" ] and cmd_name in self .subcommands :
1470
+ return # menus don't have subcommands you smooth brain
1471
+
1472
+ selected_cmd = self .commands ["context" ][cmd_name ]
1473
+
1474
+ if (
1475
+ selected_cmd .allowed_guild_ids
1476
+ and ctx .guild_id not in selected_cmd .allowed_guild_ids
1477
+ ):
1478
+ return
1479
+
1480
+ if selected_cmd .has_subcommands and not selected_cmd .func :
1481
+ return await self .handle_subcommand (ctx , to_use )
1482
+
1483
+ if "options" in to_use ["data" ]:
1484
+ for x in to_use ["data" ]["options" ]:
1485
+ if "value" not in x :
1486
+ return await self .handle_subcommand (ctx , to_use )
1487
+
1488
+ self ._discord .dispatch ("context_menu" , ctx )
1489
+
1490
+ await self .invoke_command (selected_cmd , ctx , args = {})
1491
+
1322
1492
async def handle_subcommand (self , ctx : context .SlashContext , data : dict ):
1323
1493
"""
1324
1494
Coroutine for handling subcommand.
0 commit comments