@@ -29,6 +29,8 @@ class SlashCommand:
29
29
:type client: Union[discord.Client, discord.ext.commands.Bot]
30
30
:param sync_commands: Whether to sync commands automatically. Default `False`.
31
31
:type sync_commands: bool
32
+ :param debug_guild: Guild ID of guild to use for testing commands. Prevents setting global commands in favor of guild commands, which update instantly
33
+ :type debug_guild: int
32
34
:param delete_from_unused_guilds: If the bot should make a request to set no commands for guilds that haven't got any commands registered in :class:``SlashCommand``. Default `False`.
33
35
:type delete_from_unused_guilds: bool
34
36
:param sync_on_cog_reload: Whether to sync commands on cog reload. Default `False`.
@@ -44,6 +46,7 @@ class SlashCommand:
44
46
45
47
:ivar _discord: Discord client of this client.
46
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.
47
50
:ivar req: :class:`.http.SlashCommandRequest` of this client.
48
51
:ivar logger: Logger of this client.
49
52
:ivar sync_commands: Whether to sync commands automatically.
@@ -55,18 +58,20 @@ def __init__(
55
58
self ,
56
59
client : typing .Union [discord .Client , commands .Bot ],
57
60
sync_commands : bool = False ,
61
+ debug_guild : typing .Optional [int ] = None ,
58
62
delete_from_unused_guilds : bool = False ,
59
63
sync_on_cog_reload : bool = False ,
60
64
override_type : bool = False ,
61
65
application_id : typing .Optional [int ] = None ,
62
66
):
63
67
self ._discord = client
64
- self .commands = {}
68
+ self .commands = {"context" : {} }
65
69
self .subcommands = {}
66
70
self .components = {}
67
71
self .logger = logging .getLogger ("discord_slash" )
68
72
self .req = http .SlashCommandRequest (self .logger , self ._discord , application_id )
69
73
self .sync_commands = sync_commands
74
+ self .debug_guild = debug_guild
70
75
self .sync_on_cog_reload = sync_on_cog_reload
71
76
72
77
if self .sync_commands :
@@ -266,12 +271,53 @@ async def to_dict(self):
266
271
await self ._discord .wait_until_ready () # In case commands are still not registered to SlashCommand.
267
272
all_guild_ids = []
268
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
269
282
for i in self .commands [x ].allowed_guild_ids :
270
283
if i not in all_guild_ids :
271
284
all_guild_ids .append (i )
272
285
cmds = {"global" : [], "guild" : {x : [] for x in all_guild_ids }}
273
286
wait = {} # Before merging to return dict, let's first put commands to temporary dict.
274
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
+
275
321
selected = self .commands [x ]
276
322
if selected .allowed_guild_ids :
277
323
for y in selected .allowed_guild_ids :
@@ -283,7 +329,10 @@ async def to_dict(self):
283
329
"options" : selected .options or [],
284
330
"default_permission" : selected .default_permission ,
285
331
"permissions" : {},
332
+ "type" : selected ._type ,
286
333
}
334
+ if command_dict ["type" ] != 1 :
335
+ command_dict .pop ("description" )
287
336
if y in selected .permissions :
288
337
command_dict ["permissions" ][y ] = selected .permissions [y ]
289
338
wait [y ][x ] = copy .deepcopy (command_dict )
@@ -296,14 +345,20 @@ async def to_dict(self):
296
345
"options" : selected .options or [],
297
346
"default_permission" : selected .default_permission ,
298
347
"permissions" : selected .permissions or {},
348
+ "type" : selected ._type ,
299
349
}
350
+ if command_dict ["type" ] != 1 :
351
+ command_dict .pop ("description" )
300
352
wait ["global" ][x ] = copy .deepcopy (command_dict )
301
353
302
354
# Separated normal command add and subcommand add not to
303
355
# merge subcommands to one. More info at Issue #88
304
356
# https://github.com/eunwoo1104/discord-py-slash-command/issues/88
305
357
306
358
for x in self .commands :
359
+ if x == "context" :
360
+ continue # no menus have subcommands.
361
+
307
362
if not self .commands [x ].has_subcommands :
308
363
continue
309
364
tgt = self .subcommands [x ]
@@ -373,7 +428,8 @@ async def sync_all_commands(
373
428
permissions_map = {}
374
429
cmds = await self .to_dict ()
375
430
self .logger .info ("Syncing commands..." )
376
- cmds_formatted = {None : cmds ["global" ]}
431
+ # if debug_guild is set, global commands get re-routed to the guild to update quickly
432
+ cmds_formatted = {self .debug_guild : cmds ["global" ]}
377
433
for guild in cmds ["guild" ]:
378
434
cmds_formatted [guild ] = cmds ["guild" ][guild ]
379
435
@@ -419,7 +475,7 @@ async def sync_all_commands(
419
475
if ex .status == 400 :
420
476
# catch bad requests
421
477
cmd_nums = set (
422
- re .findall (r"In\s(\d). " , ex .args [0 ])
478
+ re .findall (r"^[\w-]{1,32}$ " , ex .args [0 ])
423
479
) # find all discords references to commands
424
480
error_string = ex .args [0 ]
425
481
@@ -589,6 +645,66 @@ def add_slash_command(
589
645
self .logger .debug (f"Added command `{ name } `" )
590
646
return obj
591
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
+
592
708
def add_subcommand (
593
709
self ,
594
710
cmd ,
@@ -911,6 +1027,34 @@ def wrapper(cmd):
911
1027
912
1028
return wrapper
913
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
+
914
1058
def add_component_callback (
915
1059
self ,
916
1060
callback : typing .Coroutine ,
@@ -1250,12 +1394,15 @@ async def on_socket_response(self, msg):
1250
1394
1251
1395
to_use = msg ["d" ]
1252
1396
interaction_type = to_use ["type" ]
1253
- if interaction_type in (1 , 2 ):
1254
- return await self ._on_slash (to_use )
1255
- if interaction_type == 3 :
1256
- return await self ._on_component (to_use )
1257
-
1258
- 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
1259
1406
1260
1407
async def _on_component (self , to_use ):
1261
1408
ctx = context .ComponentContext (self .req , to_use , self ._discord , self .logger )
@@ -1314,6 +1461,34 @@ async def _on_slash(self, to_use):
1314
1461
1315
1462
await self .invoke_command (selected_cmd , ctx , args )
1316
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
+
1317
1492
async def handle_subcommand (self , ctx : context .SlashContext , data : dict ):
1318
1493
"""
1319
1494
Coroutine for handling subcommand.
0 commit comments