1 | # Copyright Pincer 2021-Present |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
2 | # Full MIT License can be found in `LICENSE` at the project root. |
||
3 | |||
4 | from __future__ import annotations |
||
5 | |||
6 | import logging |
||
7 | import re |
||
8 | from asyncio import iscoroutinefunction |
||
9 | from functools import partial |
||
10 | from inspect import Signature, isasyncgenfunction, _empty |
||
11 | from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union, List |
||
12 | |||
13 | from . import __package__ |
||
14 | from .chat_command_handler import ChatCommandHandler, _hash_app_command_params |
||
15 | from ..commands.arg_types import ( |
||
16 | ChannelTypes, |
||
17 | CommandArg, |
||
18 | Description, |
||
19 | Choices, |
||
20 | MaxValue, |
||
21 | MinValue, |
||
22 | ) |
||
23 | from ..commands.groups import Group, Subgroup |
||
24 | from ..utils.snowflake import Snowflake |
||
25 | from ..utils.types import APINullable, MISSING |
||
26 | from ..exceptions import ( |
||
27 | CommandIsNotCoroutine, |
||
28 | CommandAlreadyRegistered, |
||
29 | TooManyArguments, |
||
30 | InvalidArgumentAnnotation, |
||
31 | CommandDescriptionTooLong, |
||
32 | InvalidCommandGuild, |
||
33 | InvalidCommandName, |
||
34 | ) |
||
35 | from ..objects import ( |
||
36 | ThrottleScope, |
||
37 | AppCommand, |
||
38 | Role, |
||
39 | User, |
||
40 | Channel, |
||
41 | Mentionable, |
||
42 | MessageContext, |
||
43 | ) |
||
44 | from ..objects.app import ( |
||
45 | AppCommandOptionType, |
||
46 | AppCommandOption, |
||
47 | InteractableStructure, |
||
48 | AppCommandType, |
||
49 | ) |
||
50 | from ..utils import should_pass_ctx |
||
51 | from ..utils.signature import get_signature_and_params |
||
52 | |||
53 | if TYPE_CHECKING: |
||
54 | from typing import Optional |
||
55 | |||
56 | REGULAR_COMMAND_NAME_REGEX = re.compile(r"[\w\- ]{1,32}$") |
||
57 | CHAT_INPUT_COMMAND_NAME_REGEX = re.compile(r"^[a-z0-9_-]{1,32}$") |
||
58 | |||
59 | _log = logging.getLogger(__package__) |
||
60 | |||
61 | _options_type_link = { |
||
62 | Signature.empty: AppCommandOptionType.STRING, |
||
63 | str: AppCommandOptionType.STRING, |
||
64 | int: AppCommandOptionType.INTEGER, |
||
65 | bool: AppCommandOptionType.BOOLEAN, |
||
66 | float: AppCommandOptionType.NUMBER, |
||
67 | User: AppCommandOptionType.USER, |
||
68 | Channel: AppCommandOptionType.CHANNEL, |
||
69 | Role: AppCommandOptionType.ROLE, |
||
70 | Mentionable: AppCommandOptionType.MENTIONABLE, |
||
71 | } |
||
72 | |||
73 | if TYPE_CHECKING: |
||
74 | from ..client import Client |
||
75 | |||
76 | T = TypeVar("T") |
||
77 | |||
78 | |||
79 | def command( |
||
0 ignored issues
–
show
|
|||
80 | func=None, |
||
0 ignored issues
–
show
|
|||
81 | *, |
||
0 ignored issues
–
show
|
|||
82 | name: Optional[str] = None, |
||
0 ignored issues
–
show
|
|||
83 | description: Optional[str] = "Description not set", |
||
0 ignored issues
–
show
|
|||
84 | enable_default: Optional[bool] = True, |
||
0 ignored issues
–
show
|
|||
85 | guild: Union[Snowflake, int, str] = None, |
||
0 ignored issues
–
show
|
|||
86 | cooldown: Optional[int] = 0, |
||
0 ignored issues
–
show
|
|||
87 | cooldown_scale: Optional[float] = 60.0, |
||
0 ignored issues
–
show
|
|||
88 | cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, |
||
0 ignored issues
–
show
|
|||
89 | parent: Optional[Union[Group, Subgroup]] = None, |
||
0 ignored issues
–
show
|
|||
90 | ): |
||
91 | """A decorator to create a slash command to register and respond to |
||
92 | with the discord API from a function. |
||
93 | |||
94 | str - String |
||
95 | int - Integer |
||
96 | bool - Boolean |
||
97 | float - Number |
||
98 | pincer.objects.User - User |
||
99 | pincer.objects.Channel - Channel |
||
100 | pincer.objects.Role - Role |
||
101 | pincer.objects.Mentionable - Mentionable |
||
102 | |||
103 | .. code-block:: python3 |
||
104 | |||
105 | class Bot(Client): |
||
106 | @command( |
||
107 | name="test", |
||
108 | description="placeholder" |
||
109 | ) |
||
110 | async def test_command( |
||
111 | self, |
||
112 | ctx: MessageContext, |
||
113 | amount: int, |
||
114 | name: CommandArg[ |
||
115 | str, |
||
116 | Description("Do something cool"), |
||
117 | Choices(Choice("first value", 1), 5) |
||
118 | ], |
||
119 | optional_int: CommandArg[ |
||
120 | int, |
||
121 | MinValue(10), |
||
122 | MaxValue(100), |
||
123 | ] = 50 |
||
124 | ): |
||
125 | return Message( |
||
126 | f"You chose {amount}, {name}, {letter}", |
||
127 | flags=InteractionFlags.EPHEMERAL |
||
128 | ) |
||
129 | |||
130 | |||
131 | References from above: |
||
132 | :class:`~pincer.client.Client`, |
||
133 | :class:`~pincer.objects.message.message.Message`, |
||
134 | :class:`~pincer.objects.message.context.MessageContext`, |
||
135 | :class:`~pincer.objects.app.interaction_flags.InteractionFlags`, |
||
136 | :class:`~pincer.commands.arg_types.Choices`, |
||
137 | :class:`~pincer.commands.arg_types.Choice`, |
||
138 | :class:`typing_extensions.Annotated` (Python 3.8), |
||
139 | :class:`typing.Annotated` (Python 3.9+), |
||
140 | :class:`~pincer.commands.arg_types.CommandArg`, |
||
141 | :class:`~pincer.commands.arg_types.Description`, |
||
142 | :class:`~pincer.commands.arg_types.MinValue`, |
||
143 | :class:`~pincer.commands.arg_types.MaxValue` |
||
144 | |||
145 | |||
146 | Parameters |
||
147 | ---------- |
||
148 | name : Optional[:class:`str`] |
||
149 | The name of the command |default| :data:`None` |
||
150 | description : Optional[:class:`str`] |
||
151 | The description of the command |default| ``Description not set`` |
||
152 | enable_default : Optional[:class:`bool`] |
||
153 | Whether the command is enabled by default |default| :data:`True` |
||
154 | guild : Optional[Union[:class:`~pincer.utils.snowflake.Snowflake`, :class:`int`, :class:`str`]] |
||
155 | What guild to add it to (don't specify for global) |default| :data:`None` |
||
156 | cooldown : Optional[:class:`int`] |
||
157 | The amount of times in the cooldown_scale the command can be invoked |
||
158 | |default| ``0`` |
||
159 | cooldown_scale : Optional[:class:`float`] |
||
160 | The 'checking time' of the cooldown |default| ``60`` |
||
161 | cooldown_scope : :class:`~pincer.objects.app.throttle_scope.ThrottleScope` |
||
162 | What type of cooldown strategy to use |default| :attr:`ThrottleScope.USER` |
||
163 | |||
164 | Raises |
||
165 | ------ |
||
166 | CommandIsNotCoroutine |
||
167 | If the command function is not a coro |
||
168 | InvalidCommandName |
||
169 | If the command name does not follow the regex ``^[\\w-]{1,32}$`` |
||
170 | InvalidCommandGuild |
||
171 | If the guild id is invalid |
||
172 | CommandDescriptionTooLong |
||
173 | Descriptions max 100 characters |
||
174 | If the annotation on an argument is too long (also max 100) |
||
175 | CommandAlreadyRegistered |
||
176 | If the command already exists |
||
177 | TooManyArguments |
||
178 | Max 25 arguments to pass for commands |
||
179 | InvalidArgumentAnnotation |
||
180 | Annotation amount is max 25, |
||
181 | Not a valid argument type, |
||
182 | Annotations must consist of name and value |
||
183 | """ # noqa: E501 |
||
184 | if func is None: |
||
185 | return partial( |
||
186 | command, |
||
187 | name=name, |
||
188 | description=description, |
||
189 | enable_default=enable_default, |
||
190 | guild=guild, |
||
191 | cooldown=cooldown, |
||
192 | cooldown_scale=cooldown_scale, |
||
193 | cooldown_scope=cooldown_scope, |
||
194 | parent=parent, |
||
195 | ) |
||
196 | |||
197 | cmd = name or func.__name__ |
||
198 | |||
199 | if not re.match(CHAT_INPUT_COMMAND_NAME_REGEX, cmd): |
||
200 | raise InvalidCommandName( |
||
201 | f"Command `{cmd}` doesn't follow the name requirements." |
||
202 | " Ensure to match the following regex:" |
||
203 | f" {CHAT_INPUT_COMMAND_NAME_REGEX.pattern}" |
||
204 | ) |
||
205 | |||
206 | options: List[AppCommandOption] = [] |
||
207 | |||
208 | signature, params = get_signature_and_params(func) |
||
209 | pass_context = should_pass_ctx(signature, params) |
||
210 | |||
211 | if len(params) > (25 + pass_context): |
||
212 | cmd = name or func.__name__ |
||
213 | raise TooManyArguments( |
||
214 | f"Command `{cmd}` (`{func.__name__}`) can only have 25 " |
||
215 | f"arguments (excluding the context and self) yet {len(params)} " |
||
216 | "were provided!" |
||
217 | ) |
||
218 | |||
219 | for idx, param in enumerate(params): |
||
220 | if idx == 0 and pass_context: |
||
221 | continue |
||
222 | |||
223 | sig = signature[param] |
||
224 | |||
225 | annotation, required = sig.annotation, sig.default is _empty |
||
226 | |||
227 | # ctx is type MessageContext but should not be included in the |
||
228 | # slash command |
||
229 | if annotation == MessageContext and idx == 1: |
||
230 | return |
||
231 | |||
232 | argument_type = None |
||
233 | if type(annotation) is CommandArg: |
||
0 ignored issues
–
show
|
|||
234 | argument_type = annotation.command_type |
||
235 | # isinstance and type don't work for Annotated. This is the best way 💀 |
||
236 | elif hasattr(annotation, "__metadata__"): |
||
237 | # typing.get_origin doesn't work in 3.9+ for some reason. Maybe they forgor |
||
238 | # to implement it. |
||
239 | argument_type = annotation.__origin__ |
||
240 | |||
241 | # check if None to keep the type checker happy that its Any |
||
242 | if argument_type is not None and hasattr(argument_type, "__args__"): |
||
243 | # this is a Union, hopefully an Optional/Union[T, None] |
||
244 | # Optional[T] is an alias to Union[T, None] |
||
245 | args = argument_type.__args__ |
||
246 | |||
247 | if len(args) != 2 or args[1] != type(None): |
||
248 | raise InvalidArgumentAnnotation( |
||
249 | "`Union` is not a supported option type" |
||
250 | ) |
||
251 | |||
252 | argument_type = args[0] |
||
253 | |||
254 | if not argument_type: |
||
255 | if annotation in _options_type_link: |
||
256 | options.append( |
||
257 | AppCommandOption( |
||
258 | type=_options_type_link[annotation], |
||
259 | name=param, |
||
260 | description="Description not set", |
||
261 | required=required, |
||
262 | ) |
||
263 | ) |
||
264 | continue |
||
265 | |||
266 | # TODO: Write better exception |
||
0 ignored issues
–
show
|
|||
267 | raise InvalidArgumentAnnotation( |
||
268 | "Type must be Annotated or other valid type" |
||
269 | ) |
||
270 | |||
271 | command_type = _options_type_link[argument_type] |
||
272 | |||
273 | def get_arg(t: T) -> APINullable[T]: |
||
0 ignored issues
–
show
|
|||
274 | if type(annotation) is CommandArg: |
||
0 ignored issues
–
show
|
|||
275 | return annotation.get_arg(t) |
||
0 ignored issues
–
show
|
|||
276 | elif hasattr(annotation, "__metadata__"): |
||
0 ignored issues
–
show
|
|||
277 | for obj in annotation.__metadata__: |
||
0 ignored issues
–
show
|
|||
278 | if isinstance(obj, t): |
||
279 | return obj.get_payload() |
||
280 | return MISSING |
||
281 | |||
282 | argument_description = get_arg(Description) or "Description not set" |
||
283 | |||
284 | choices = get_arg(Choices) |
||
285 | |||
286 | if choices is not MISSING and argument_type not in { |
||
287 | int, |
||
0 ignored issues
–
show
|
|||
288 | float, |
||
0 ignored issues
–
show
|
|||
289 | str, |
||
0 ignored issues
–
show
|
|||
290 | }: |
||
291 | raise InvalidArgumentAnnotation( |
||
292 | "Choice type is only allowed for str, int, and float" |
||
293 | ) |
||
294 | if choices is not MISSING: |
||
295 | for choice in choices: |
||
296 | if isinstance(choice.value, int) and argument_type is float: |
||
297 | continue |
||
298 | if not isinstance(choice.value, argument_type): |
||
299 | raise InvalidArgumentAnnotation( |
||
300 | "Choice value must match the command type" |
||
301 | ) |
||
302 | |||
303 | channel_types = get_arg(ChannelTypes) |
||
304 | if channel_types is not MISSING and argument_type is not Channel: |
||
305 | raise InvalidArgumentAnnotation( |
||
306 | "ChannelTypes are only available for Channels" |
||
307 | ) |
||
308 | |||
309 | max_value = get_arg(MaxValue) |
||
310 | min_value = get_arg(MinValue) |
||
311 | |||
312 | for i, value in enumerate((min_value, max_value)): |
||
313 | if ( |
||
314 | value is not MISSING |
||
0 ignored issues
–
show
|
|||
315 | and argument_type is not int |
||
0 ignored issues
–
show
|
|||
316 | and argument_type is not float |
||
0 ignored issues
–
show
|
|||
317 | ): |
||
318 | t = ("MinValue", "MaxValue") |
||
319 | raise InvalidArgumentAnnotation( |
||
320 | f"{t[i]} is only available for int and float" |
||
321 | ) |
||
322 | |||
323 | options.append( |
||
324 | AppCommandOption( |
||
325 | type=command_type, |
||
326 | name=param, |
||
327 | description=argument_description, |
||
328 | required=required, |
||
329 | choices=choices, |
||
330 | channel_types=channel_types, |
||
331 | max_value=max_value, |
||
332 | min_value=min_value, |
||
333 | ) |
||
334 | ) |
||
335 | |||
336 | # Discord API returns MISSING for options when there are 0. Options is set MISSING |
||
337 | # so equality checks later work properly. |
||
338 | if not options: |
||
339 | options = MISSING |
||
340 | |||
341 | return register_command( |
||
342 | func=func, |
||
343 | app_command_type=AppCommandType.CHAT_INPUT, |
||
344 | name=name, |
||
345 | description=description, |
||
346 | enable_default=enable_default, |
||
347 | guild=guild, |
||
348 | cooldown=cooldown, |
||
349 | cooldown_scale=cooldown_scale, |
||
350 | cooldown_scope=cooldown_scope, |
||
351 | command_options=options, |
||
352 | parent=parent, |
||
353 | ) |
||
354 | |||
355 | |||
356 | def user_command( |
||
357 | func=None, |
||
0 ignored issues
–
show
|
|||
358 | *, |
||
0 ignored issues
–
show
|
|||
359 | name: Optional[str] = None, |
||
0 ignored issues
–
show
|
|||
360 | enable_default: Optional[bool] = True, |
||
0 ignored issues
–
show
|
|||
361 | guild: Union[Snowflake, int, str] = None, |
||
0 ignored issues
–
show
|
|||
362 | cooldown: Optional[int] = 0, |
||
0 ignored issues
–
show
|
|||
363 | cooldown_scale: Optional[float] = 60, |
||
0 ignored issues
–
show
|
|||
364 | cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, |
||
0 ignored issues
–
show
|
|||
365 | ): |
||
366 | """A decorator to create a user command registering and responding |
||
367 | to the Discord API from a function. |
||
368 | |||
369 | .. code-block:: python3 |
||
370 | |||
371 | class Bot(Client): |
||
372 | @user_command |
||
373 | async def test_user_command( |
||
374 | self, |
||
375 | ctx: MessageContext, |
||
376 | user: User, |
||
377 | member: GuildMember |
||
378 | ): |
||
379 | if not member: |
||
380 | # member is missing if this is a DM |
||
381 | # This bot doesn't like being DMed, so it won't respond |
||
382 | return |
||
383 | |||
384 | return f"Hello {user.name}, this is a Guild." |
||
385 | |||
386 | |||
387 | References from above: |
||
388 | :class:`~client.Client`, |
||
389 | :class:`~objects.message.context.MessageContext`, |
||
390 | :class:`~objects.user.user.User`, |
||
391 | :class:`~objects.guild.member.GuildMember`, |
||
392 | |||
393 | |||
394 | Parameters |
||
395 | ---------- |
||
396 | name : Optional[:class:`str`] |
||
397 | The name of the command |default| :data:`None` |
||
398 | enable_default : Optional[:class:`bool`] |
||
399 | Whether the command is enabled by default |default| :data:`True` |
||
400 | guild : Optional[Union[:class:`~pincer.utils.snowflake.Snowflake`, :class:`int`, :class:`str`]] |
||
401 | What guild to add it to (don't specify for global) |default| :data:`None` |
||
402 | cooldown : Optional[:class:`int`] |
||
403 | The amount of times in the cooldown_scale the command can be invoked |
||
404 | |default| ``0`` |
||
405 | cooldown_scale : Optional[:class:`float`] |
||
406 | The 'checking time' of the cooldown |default| ``60`` |
||
407 | cooldown_scope : :class:`~pincer.objects.app.throttle_scope.ThrottleScope` |
||
408 | What type of cooldown strategy to use |default| :attr:`ThrottleScope.USER` |
||
409 | |||
410 | Raises |
||
411 | ------ |
||
412 | CommandIsNotCoroutine |
||
413 | If the command function is not a coro |
||
414 | InvalidCommandName |
||
415 | If the command name does not follow the regex ``^[\\w-]{1,32}$`` |
||
416 | InvalidCommandGuild |
||
417 | If the guild id is invalid |
||
418 | CommandDescriptionTooLong |
||
419 | Descriptions max 100 characters |
||
420 | If the annotation on an argument is too long (also max 100) |
||
421 | CommandAlreadyRegistered |
||
422 | If the command already exists |
||
423 | InvalidArgumentAnnotation |
||
424 | Annotation amount is max 25, |
||
425 | Not a valid argument type, |
||
426 | Annotations must consist of name and value |
||
427 | """ # noqa: E501 |
||
428 | if func is None: |
||
429 | return partial( |
||
430 | user_command, |
||
431 | name=name, |
||
432 | enable_default=enable_default, |
||
433 | guild=guild, |
||
434 | cooldown=cooldown, |
||
435 | cooldown_scale=cooldown_scale, |
||
436 | cooldown_scope=cooldown_scope, |
||
437 | ) |
||
438 | |||
439 | return register_command( |
||
440 | func=func, |
||
441 | app_command_type=AppCommandType.USER, |
||
442 | name=name, |
||
443 | enable_default=enable_default, |
||
444 | guild=guild, |
||
445 | cooldown=cooldown, |
||
446 | cooldown_scale=cooldown_scale, |
||
447 | cooldown_scope=cooldown_scope, |
||
448 | ) |
||
449 | |||
450 | |||
451 | def message_command( |
||
452 | func=None, |
||
0 ignored issues
–
show
|
|||
453 | *, |
||
0 ignored issues
–
show
|
|||
454 | name: Optional[str] = None, |
||
0 ignored issues
–
show
|
|||
455 | enable_default: Optional[bool] = True, |
||
0 ignored issues
–
show
|
|||
456 | guild: Union[Snowflake, int, str] = None, |
||
0 ignored issues
–
show
|
|||
457 | cooldown: Optional[int] = 0, |
||
0 ignored issues
–
show
|
|||
458 | cooldown_scale: Optional[float] = 60, |
||
0 ignored issues
–
show
|
|||
459 | cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, |
||
0 ignored issues
–
show
|
|||
460 | ): |
||
461 | """A decorator to create a user command to register and respond |
||
462 | to the Discord API from a function. |
||
463 | |||
464 | .. code-block:: python3 |
||
465 | |||
466 | class Bot(Client): |
||
467 | @user_command |
||
468 | async def test_message_command( |
||
469 | self, |
||
470 | ctx: MessageContext, |
||
471 | message: UserMessage, |
||
472 | ): |
||
473 | return message.content |
||
474 | |||
475 | |||
476 | References from above: |
||
477 | :class:`~client.Client`, |
||
478 | :class:`~objects.message.context.MessageContext`, |
||
479 | :class:`~objects.message.message.UserMessage`, |
||
480 | :class:`~objects.user.user.User`, |
||
481 | :class:`~objects.guild.member.GuildMember`, |
||
482 | |||
483 | |||
484 | Parameters |
||
485 | ---------- |
||
486 | name : Optional[:class:`str`] |
||
487 | The name of the command |default| :data:`None` |
||
488 | enable_default : Optional[:class:`bool`] |
||
489 | Whether the command is enabled by default |default| :data:`True` |
||
490 | guild : Optional[Union[:class:`~pincer.utils.snowflake.Snowflake`, :class:`int`, :class:`str`]] |
||
491 | What guild to add it to (don't specify for global) |default| :data:`None` |
||
492 | cooldown : Optional[:class:`int`] |
||
493 | The amount of times in the cooldown_scale the command can be invoked |
||
494 | |default| ``0`` |
||
495 | cooldown_scale : Optional[:class:`float`] |
||
496 | The 'checking time' of the cooldown |default| ``60`` |
||
497 | cooldown_scope : :class:`~pincer.objects.app.throttle_scope.ThrottleScope` |
||
498 | What type of cooldown strategy to use |default| :attr:`ThrottleScope.USER` |
||
499 | |||
500 | Raises |
||
501 | ------ |
||
502 | CommandIsNotCoroutine |
||
503 | If the command function is not a coro |
||
504 | InvalidCommandName |
||
505 | If the command name does not follow the regex ``^[\\w-]{1,32}$`` |
||
506 | InvalidCommandGuild |
||
507 | If the guild id is invalid |
||
508 | CommandDescriptionTooLong |
||
509 | Descriptions max 100 characters |
||
510 | If the annotation on an argument is too long (also max 100) |
||
511 | CommandAlreadyRegistered |
||
512 | If the command already exists |
||
513 | InvalidArgumentAnnotation |
||
514 | Annotation amount is max 25, |
||
515 | Not a valid argument type, |
||
516 | Annotations must consist of name and value |
||
517 | """ # noqa: E501 |
||
518 | if func is None: |
||
519 | return partial( |
||
520 | message_command, |
||
521 | name=name, |
||
522 | enable_default=enable_default, |
||
523 | guild=guild, |
||
524 | cooldown=cooldown, |
||
525 | cooldown_scale=cooldown_scale, |
||
526 | cooldown_scope=cooldown_scope, |
||
527 | ) |
||
528 | |||
529 | return register_command( |
||
530 | func=func, |
||
531 | app_command_type=AppCommandType.MESSAGE, |
||
532 | name=name, |
||
533 | enable_default=enable_default, |
||
534 | guild=guild, |
||
535 | cooldown=cooldown, |
||
536 | cooldown_scale=cooldown_scale, |
||
537 | cooldown_scope=cooldown_scope, |
||
538 | ) |
||
539 | |||
540 | |||
541 | def register_command( |
||
0 ignored issues
–
show
|
|||
542 | func: Callable[..., Any] = None, |
||
0 ignored issues
–
show
|
|||
543 | app_command_type: Optional[AppCommandType] = None, |
||
0 ignored issues
–
show
|
|||
544 | name: Optional[str] = None, |
||
0 ignored issues
–
show
|
|||
545 | description: Optional[str] = MISSING, |
||
0 ignored issues
–
show
|
|||
546 | enable_default: Optional[bool] = True, |
||
0 ignored issues
–
show
|
|||
547 | guild: Optional[Union[Snowflake, int, str]] = None, |
||
0 ignored issues
–
show
|
|||
548 | cooldown: Optional[int] = 0, |
||
0 ignored issues
–
show
|
|||
549 | cooldown_scale: Optional[float] = 60.0, |
||
0 ignored issues
–
show
|
|||
550 | cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER, |
||
0 ignored issues
–
show
|
|||
551 | command_options=MISSING, # Missing typehint? |
||
0 ignored issues
–
show
|
|||
552 | parent: Optional[Union[Group, Subgroup]] = MISSING, |
||
0 ignored issues
–
show
|
|||
553 | ): |
||
554 | cmd = name or func.__name__ |
||
555 | |||
556 | if not re.match(REGULAR_COMMAND_NAME_REGEX, cmd): |
||
557 | raise InvalidCommandName( |
||
558 | f"Command `{cmd}` doesn't follow the name requirements." |
||
559 | " Ensure to match the following regex:" |
||
560 | f" {REGULAR_COMMAND_NAME_REGEX.pattern}" |
||
561 | ) |
||
562 | |||
563 | if not iscoroutinefunction(func) and not isasyncgenfunction(func): |
||
564 | raise CommandIsNotCoroutine( |
||
565 | f"Command with call `{func.__name__}` is not a coroutine, " |
||
566 | "which is required for commands." |
||
567 | ) |
||
568 | |||
569 | try: |
||
570 | guild_id = Snowflake(guild) if guild else MISSING |
||
571 | except ValueError: |
||
572 | raise InvalidCommandGuild( |
||
573 | f"Command with call `{func.__name__}` its `guilds` parameter " |
||
574 | "contains a non valid guild id." |
||
575 | ) |
||
576 | |||
577 | if description and len(description) > 100: |
||
578 | raise CommandDescriptionTooLong( |
||
579 | f"Command `{cmd}` (`{func.__name__}`) its description exceeds " |
||
580 | "the 100 character limit." |
||
581 | ) |
||
582 | |||
583 | group = MISSING |
||
584 | sub_group = MISSING |
||
585 | |||
586 | if isinstance(parent, Group): |
||
587 | group = parent |
||
588 | if isinstance(parent, Subgroup): |
||
589 | group = parent.parent |
||
590 | sub_group = parent |
||
591 | |||
592 | if reg := ChatCommandHandler.register.get( |
||
593 | _hash_app_command_params(cmd, guild, app_command_type, group, sub_group) |
||
0 ignored issues
–
show
|
|||
594 | ): |
||
595 | raise CommandAlreadyRegistered( |
||
596 | f"Command `{cmd}` (`{func.__name__}`) has already been " |
||
597 | f"registered by `{reg.call.__name__}`." |
||
598 | ) |
||
599 | |||
600 | _log.info(f"Registered command `{cmd}` to `{func.__name__}` locally.") |
||
0 ignored issues
–
show
|
|||
601 | |||
602 | interactable = InteractableStructure( |
||
603 | call=func, |
||
604 | cooldown=cooldown, |
||
605 | cooldown_scale=cooldown_scale, |
||
606 | cooldown_scope=cooldown_scope, |
||
607 | manager=None, |
||
608 | group=group, |
||
609 | sub_group=sub_group, |
||
610 | metadata=AppCommand( |
||
611 | name=cmd, |
||
612 | description=description, |
||
613 | type=app_command_type, |
||
614 | default_permission=enable_default, |
||
615 | options=command_options, |
||
616 | guild_id=guild_id, |
||
617 | ), |
||
618 | ) |
||
619 | |||
620 | ChatCommandHandler.register[ |
||
621 | _hash_app_command_params( |
||
622 | cmd, guild_id, app_command_type, group, sub_group |
||
623 | ) |
||
624 | ] = interactable |
||
625 | |||
626 | return interactable |
||
627 |