Skip to content

Commit c2bafa3

Browse files
authored
refactor: create an extra function for command payload checking (#529)
* a * refactor: create an extra function for command payload checking * refactor: extract command checking into seperate function * fix!: change convertion and prevent NoneType error * refactor: update regex to not allow uppercase letters * refactor: update regex * refactor: add check for context menus and fix typo * fix!: add missing ``not`` * Add check for duplicated option names on same command * refactor: update regex in pyi * refactor: correct description check * refactor: check for `**kwargs` in coro check * refactor: indent debug strings to create a visual separation * remove leftover # * refactor: correct logging statement * fix!: option parsing
1 parent 28e408d commit c2bafa3

File tree

2 files changed

+213
-79
lines changed

2 files changed

+213
-79
lines changed

interactions/client.py

Lines changed: 207 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import sys
23
from asyncio import get_event_loop, iscoroutinefunction
34
from 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]

interactions/client.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ class Client:
4646
async def _ready(self) -> None: ...
4747
async def _login(self) -> None: ...
4848
def event(self, coro: Coroutine, name: Optional[str] = None) -> Callable[..., Any]: ...
49+
def __check_command(
50+
self,
51+
command: ApplicationCommand,
52+
coro: Coroutine,
53+
regex: str = r"^[a-z0-9_-]{1,32}$",
54+
)-> None: ...
4955
def command(
5056
self,
5157
*,

0 commit comments

Comments
 (0)