@@ -46,6 +46,7 @@ class SlashCommand:
4646
4747 :ivar _discord: Discord client of this client.
4848 :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.
4950 :ivar req: :class:`.http.SlashCommandRequest` of this client.
5051 :ivar logger: Logger of this client.
5152 :ivar sync_commands: Whether to sync commands automatically.
@@ -64,7 +65,7 @@ def __init__(
6465 application_id : typing .Optional [int ] = None ,
6566 ):
6667 self ._discord = client
67- self .commands = {}
68+ self .commands = {"context" : {} }
6869 self .subcommands = {}
6970 self .components = {}
7071 self .logger = logging .getLogger ("discord_slash" )
@@ -270,12 +271,53 @@ async def to_dict(self):
270271 await self ._discord .wait_until_ready () # In case commands are still not registered to SlashCommand.
271272 all_guild_ids = []
272273 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
273282 for i in self .commands [x ].allowed_guild_ids :
274283 if i not in all_guild_ids :
275284 all_guild_ids .append (i )
276285 cmds = {"global" : [], "guild" : {x : [] for x in all_guild_ids }}
277286 wait = {} # Before merging to return dict, let's first put commands to temporary dict.
278287 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+
279321 selected = self .commands [x ]
280322 if selected .allowed_guild_ids :
281323 for y in selected .allowed_guild_ids :
@@ -287,7 +329,10 @@ async def to_dict(self):
287329 "options" : selected .options or [],
288330 "default_permission" : selected .default_permission ,
289331 "permissions" : {},
332+ "type" : selected ._type ,
290333 }
334+ if command_dict ["type" ] != 1 :
335+ command_dict .pop ("description" )
291336 if y in selected .permissions :
292337 command_dict ["permissions" ][y ] = selected .permissions [y ]
293338 wait [y ][x ] = copy .deepcopy (command_dict )
@@ -300,14 +345,20 @@ async def to_dict(self):
300345 "options" : selected .options or [],
301346 "default_permission" : selected .default_permission ,
302347 "permissions" : selected .permissions or {},
348+ "type" : selected ._type ,
303349 }
350+ if command_dict ["type" ] != 1 :
351+ command_dict .pop ("description" )
304352 wait ["global" ][x ] = copy .deepcopy (command_dict )
305353
306354 # Separated normal command add and subcommand add not to
307355 # merge subcommands to one. More info at Issue #88
308356 # https://github.com/eunwoo1104/discord-py-slash-command/issues/88
309357
310358 for x in self .commands :
359+ if x == "context" :
360+ continue # no menus have subcommands.
361+
311362 if not self .commands [x ].has_subcommands :
312363 continue
313364 tgt = self .subcommands [x ]
@@ -424,7 +475,7 @@ async def sync_all_commands(
424475 if ex .status == 400 :
425476 # catch bad requests
426477 cmd_nums = set (
427- re .findall (r"In\s(\d). " , ex .args [0 ])
478+ re .findall (r"^[\w-]{1,32}$ " , ex .args [0 ])
428479 ) # find all discords references to commands
429480 error_string = ex .args [0 ]
430481
@@ -594,6 +645,66 @@ def add_slash_command(
594645 self .logger .debug (f"Added command `{ name } `" )
595646 return obj
596647
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+
597708 def add_subcommand (
598709 self ,
599710 cmd ,
@@ -916,6 +1027,34 @@ def wrapper(cmd):
9161027
9171028 return wrapper
9181029
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+
9191058 def add_component_callback (
9201059 self ,
9211060 callback : typing .Coroutine ,
@@ -1255,12 +1394,15 @@ async def on_socket_response(self, msg):
12551394
12561395 to_use = msg ["d" ]
12571396 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
12641406
12651407 async def _on_component (self , to_use ):
12661408 ctx = context .ComponentContext (self .req , to_use , self ._discord , self .logger )
@@ -1319,6 +1461,34 @@ async def _on_slash(self, to_use):
13191461
13201462 await self .invoke_command (selected_cmd , ctx , args )
13211463
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+
13221492 async def handle_subcommand (self , ctx : context .SlashContext , data : dict ):
13231493 """
13241494 Coroutine for handling subcommand.
0 commit comments