1+ import re
12import sys
23from asyncio import get_event_loop , iscoroutinefunction
34from functools import wraps
@@ -319,6 +320,208 @@ def event(self, coro: Coroutine, name: Optional[str] = MISSING) -> Callable[...,
319320 self ._websocket .dispatch .register (coro , name if name is not MISSING else coro .__name__ )
320321 return coro
321322
323+ def __check_command (
324+ self ,
325+ command : ApplicationCommand ,
326+ coro : Coroutine ,
327+ regex : str = r"^[a-z0-9_-]{1,32}$" ,
328+ ) -> None :
329+ """
330+ Checks if a command is valid.
331+ """
332+ reg = re .compile (regex )
333+ _options_names : List [str ] = []
334+ _sub_groups_present : bool = False
335+ _sub_cmds_present : bool = False
336+
337+ def __check_sub_group (_sub_group : Option ):
338+ nonlocal _sub_groups_present
339+ _sub_groups_present = True
340+ if _sub_group .name is MISSING :
341+ raise InteractionException (11 , message = "Sub command groups must have a name." )
342+ __indent = 4
343+ log .debug (
344+ f"{ ' ' * __indent } checking sub command group '{ _sub_group .name } ' of command '{ command .name } '"
345+ )
346+ if not re .fullmatch (reg , _sub_group .name ):
347+ raise InteractionException (
348+ 11 ,
349+ message = f"The sub command group name does not match the regex for valid names ('{ regex } ')" ,
350+ )
351+ elif _sub_group .description is MISSING and not _sub_group .description :
352+ raise InteractionException (11 , message = "A description is required." )
353+ elif len (_sub_group .description ) > 100 :
354+ raise InteractionException (
355+ 11 , message = "Descriptions must be less than 100 characters."
356+ )
357+ if not _sub_group .options :
358+ raise InteractionException (11 , message = "sub command groups must have subcommands!" )
359+ if len (_sub_group .options ) > 25 :
360+ raise InteractionException (
361+ 11 , message = "A sub command group cannot contain more than 25 sub commands!"
362+ )
363+ for _sub_command in _sub_group .options :
364+ __check_sub_command (Option (** _sub_command ), _sub_group )
365+
366+ def __check_sub_command (_sub_command : Option , _sub_group : Option = MISSING ):
367+ nonlocal _sub_cmds_present
368+ _sub_cmds_present = True
369+ if _sub_command .name is MISSING :
370+ raise InteractionException (11 , message = "sub commands must have a name!" )
371+ if _sub_group is not MISSING :
372+ __indent = 8
373+ log .debug (
374+ f"{ ' ' * __indent } checking sub command '{ _sub_command .name } ' of group '{ _sub_group .name } '"
375+ )
376+ else :
377+ __indent = 4
378+ log .debug (
379+ f"{ ' ' * __indent } checking sub command '{ _sub_command .name } ' of command '{ command .name } '"
380+ )
381+ if not re .fullmatch (reg , _sub_command .name ):
382+ raise InteractionException (
383+ 11 ,
384+ message = f"The sub command name does not match the regex for valid names ('{ reg } ')" ,
385+ )
386+ elif _sub_command .description is MISSING or not _sub_command .description :
387+ raise InteractionException (11 , message = "A description is required." )
388+ elif len (_sub_command .description ) > 100 :
389+ raise InteractionException (
390+ 11 , message = "Descriptions must be less than 100 characters."
391+ )
392+ if _sub_command .options is not MISSING :
393+ if len (_sub_command .options ) > 25 :
394+ raise InteractionException (
395+ 11 , message = "Your sub command must have less than 25 options."
396+ )
397+ _sub_opt_names = []
398+ for _opt in _sub_command .options :
399+ __check_options (Option (** _opt ), _sub_opt_names , _sub_command )
400+ del _sub_opt_names
401+
402+ def __check_options (_option : Option , _names : list , _sub_command : Option = MISSING ):
403+ nonlocal _options_names
404+ if getattr (_option , "autocomplete" , False ) and getattr (_option , "choices" , False ):
405+ log .warning ("Autocomplete may not be set to true if choices are present." )
406+ if _option .name is MISSING :
407+ raise InteractionException (11 , message = "Options must have a name." )
408+ if _sub_command is not MISSING :
409+ __indent = 8 if not _sub_groups_present else 12
410+ log .debug (
411+ f"{ ' ' * __indent } checking option '{ _option .name } ' of sub command '{ _sub_command .name } '"
412+ )
413+ else :
414+ __indent = 4
415+ log .debug (
416+ f"{ ' ' * __indent } checking option '{ _option .name } ' of command '{ command .name } '"
417+ )
418+ _options_names .append (_option .name )
419+ if not re .fullmatch (reg , _option .name ):
420+ raise InteractionException (
421+ 11 ,
422+ message = f"The option name does not match the regex for valid names ('{ regex } ')" ,
423+ )
424+ if _option .description is MISSING or not _option .description :
425+ raise InteractionException (
426+ 11 ,
427+ message = "A description is required." ,
428+ )
429+ elif len (_option .description ) > 100 :
430+ raise InteractionException (
431+ 11 ,
432+ message = "Descriptions must be less than 100 characters." ,
433+ )
434+ if _option .name in _names :
435+ raise InteractionException (
436+ 11 , message = "You must not have two options with the same name in a command!"
437+ )
438+ _names .append (_option .name )
439+
440+ def __check_coro ():
441+ __indent = 4
442+ log .debug (f"{ ' ' * __indent } Checking coroutine: '{ coro .__name__ } '" )
443+ if not len (coro .__code__ .co_varnames ):
444+ raise InteractionException (
445+ 11 , message = "Your command needs at least one argument to return context."
446+ )
447+ elif "kwargs" in coro .__code__ .co_varnames :
448+ return
449+ elif _sub_cmds_present and len (coro .__code__ .co_varnames ) < 2 :
450+ raise InteractionException (
451+ 11 , message = "Your command needs one argument for the sub_command."
452+ )
453+ elif _sub_groups_present and len (coro .__code__ .co_varnames ) < 3 :
454+ raise InteractionException (
455+ 11 ,
456+ message = "Your command needs one argument for the sub_command and one for the sub_command_group." ,
457+ )
458+ add : int = 1 + abs (_sub_cmds_present ) + abs (_sub_groups_present )
459+
460+ if len (coro .__code__ .co_varnames ) - add < len (set (_options_names )):
461+ log .debug (
462+ "Coroutine is missing arguments for options:"
463+ f" { [_arg for _arg in _options_names if _arg not in coro .__code__ .co_varnames ]} "
464+ )
465+ raise InteractionException (
466+ 11 , message = "You need one argument for every option name in your command!"
467+ )
468+
469+ if command .name is MISSING :
470+ raise InteractionException (11 , message = "Your command must have a name." )
471+
472+ else :
473+ log .debug (f"checking command '{ command .name } ':" )
474+
475+ if (
476+ not re .fullmatch (reg , command .name )
477+ and command .type == ApplicationCommandType .CHAT_INPUT
478+ ):
479+ raise InteractionException (
480+ 11 , message = f"Your command does not match the regex for valid names ('{ regex } ')"
481+ )
482+ elif (
483+ command .type == ApplicationCommandType .CHAT_INPUT
484+ and command .description is MISSING
485+ or not command .description
486+ ):
487+ raise InteractionException (11 , message = "A description is required." )
488+ elif (
489+ command .type != ApplicationCommandType .CHAT_INPUT
490+ and command .description is not MISSING
491+ and command .description
492+ ):
493+ raise InteractionException (
494+ 11 , message = "Only chat-input commands can have a description."
495+ )
496+
497+ elif command .description is not MISSING and len (command .description ) > 100 :
498+ raise InteractionException (11 , message = "Descriptions must be less than 100 characters." )
499+
500+ if command .options and command .options is not MISSING :
501+ if len (command .options ) > 25 :
502+ raise InteractionException (
503+ 11 , message = "Your command must have less than 25 options."
504+ )
505+
506+ if command .type != ApplicationCommandType .CHAT_INPUT :
507+ raise InteractionException (
508+ 11 , message = "Only CHAT_INPUT commands can have options/sub-commands!"
509+ )
510+
511+ _opt_names = []
512+ for _option in command .options :
513+ if _option .type == OptionType .SUB_COMMAND_GROUP :
514+ __check_sub_group (_option )
515+
516+ elif _option .type == OptionType .SUB_COMMAND :
517+ __check_sub_command (_option )
518+
519+ else :
520+ __check_options (_option , _opt_names )
521+ del _opt_names
522+
523+ __check_coro ()
524+
322525 def command (
323526 self ,
324527 * ,
@@ -373,75 +576,6 @@ async def message_command(ctx):
373576 """
374577
375578 def decorator (coro : Coroutine ) -> Callable [..., Any ]:
376- if name is MISSING :
377- raise InteractionException (11 , message = "Your command must have a name." )
378-
379- elif len (name ) > 32 :
380- raise InteractionException (
381- 11 , message = "Command names must be less than 32 characters."
382- )
383- elif type == ApplicationCommandType .CHAT_INPUT and description is MISSING :
384- raise InteractionException (
385- 11 , message = "Chat-input commands must have a description."
386- )
387- elif type != ApplicationCommandType .CHAT_INPUT and description is not MISSING :
388- raise InteractionException (
389- 11 , message = "Only chat-input commands can have a description."
390- )
391-
392- elif description is not MISSING and len (description ) > 100 :
393- raise InteractionException (
394- 11 , message = "Command descriptions must be less than 100 characters."
395- )
396-
397- for _ in name :
398- if _ .isupper () and type == ApplicationCommandType .CHAT_INPUT :
399- raise InteractionException (
400- 11 ,
401- message = "Your chat-input command name must not contain uppercase characters (Discord limitation)" ,
402- )
403-
404- if not len (coro .__code__ .co_varnames ):
405- raise InteractionException (
406- 11 , message = "Your command needs at least one argument to return context."
407- )
408- if options is not MISSING :
409- if len (coro .__code__ .co_varnames ) + 1 < len (options ):
410- raise InteractionException (
411- 11 ,
412- message = "You must have the same amount of arguments as the options of the command." ,
413- )
414- if isinstance (options , List ) and len (options ) > 25 :
415- raise InteractionException (
416- 11 , message = "Your command must have less than 25 options."
417- )
418- _option : Option
419- for _option in options :
420- if _option .type not in (
421- OptionType .SUB_COMMAND ,
422- OptionType .SUB_COMMAND_GROUP ,
423- ):
424- if getattr (_option , "autocomplete" , False ) and getattr (
425- _option , "choices" , False
426- ):
427- log .warning (
428- "Autocomplete may not be set to true if choices are present."
429- )
430- if not getattr (_option , "description" , False ):
431- raise InteractionException (
432- 11 ,
433- message = "A description is required for Options that are not sub-commands." ,
434- )
435- if len (_option .description ) > 100 :
436- raise InteractionException (
437- 11 ,
438- message = "Command option descriptions must be less than 100 characters." ,
439- )
440-
441- if len (_option .name ) > 32 :
442- raise InteractionException (
443- 11 , message = "Command option names must be less than 32 characters."
444- )
445579
446580 commands : List [ApplicationCommand ] = command (
447581 type = type ,
@@ -451,6 +585,7 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
451585 options = options ,
452586 default_permission = default_permission ,
453587 )
588+ self .__check_command (command = ApplicationCommand (** commands [0 ]), coro = coro )
454589
455590 if self ._automate_sync :
456591 if self ._loop .is_running ():
@@ -505,18 +640,14 @@ async def context_menu_name(ctx):
505640 """
506641
507642 def decorator (coro : Coroutine ) -> Callable [..., Any ]:
508- if not len (coro .__code__ .co_varnames ):
509- raise InteractionException (
510- 11 ,
511- message = "Your command needs at least one argument to return context." ,
512- )
513643
514644 commands : List [ApplicationCommand ] = command (
515645 type = ApplicationCommandType .MESSAGE ,
516646 name = name ,
517647 scope = scope ,
518648 default_permission = default_permission ,
519649 )
650+ self .__check_command (ApplicationCommand (** commands [0 ]), coro )
520651
521652 if self ._automate_sync :
522653 if self ._loop .is_running ():
@@ -565,11 +696,6 @@ async def context_menu_name(ctx):
565696 """
566697
567698 def decorator (coro : Coroutine ) -> Callable [..., Any ]:
568- if not len (coro .__code__ .co_varnames ):
569- raise InteractionException (
570- 11 ,
571- message = "Your command needs at least one argument to return context." ,
572- )
573699
574700 commands : List [ApplicationCommand ] = command (
575701 type = ApplicationCommandType .USER ,
@@ -578,6 +704,8 @@ def decorator(coro: Coroutine) -> Callable[..., Any]:
578704 default_permission = default_permission ,
579705 )
580706
707+ self .__check_command (ApplicationCommand (** commands [0 ]), coro )
708+
581709 if self ._automate_sync :
582710 if self ._loop .is_running ():
583711 [self ._loop .create_task (self ._synchronize (command )) for command in commands ]
0 commit comments