Issues (1445)

pincer/commands/commands.py (60 issues)

1
# Copyright Pincer 2021-Present
0 ignored issues
show
Missing module docstring
Loading history...
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
Either all return statements in a function should return an expression, or none of them should.
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (32/15).
Loading history...
80
    func=None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
81
    *,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
82
    name: Optional[str] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
83
    description: Optional[str] = "Description not set",
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
84
    enable_default: Optional[bool] = True,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
85
    guild: Union[Snowflake, int, str] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
86
    cooldown: Optional[int] = 0,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
87
    cooldown_scale: Optional[float] = 60.0,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
88
    cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
89
    parent: Optional[Union[Group, Subgroup]] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
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
Using type() instead of isinstance() for a typecheck.
Loading history...
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
TODO and FIXME comments should generally be avoided.
Loading history...
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
Either all return statements in a function should return an expression, or none of them should.
Loading history...
274
            if type(annotation) is CommandArg:
0 ignored issues
show
Unnecessary "elif" after "return"
Loading history...
Using type() instead of isinstance() for a typecheck.
Loading history...
Cell variable annotation defined in loop
Loading history...
275
                return annotation.get_arg(t)
0 ignored issues
show
Cell variable annotation defined in loop
Loading history...
276
            elif hasattr(annotation, "__metadata__"):
0 ignored issues
show
Cell variable annotation defined in loop
Loading history...
277
                for obj in annotation.__metadata__:
0 ignored issues
show
Cell variable annotation defined in loop
Loading history...
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
Wrong hanging indentation before block (add 4 spaces).
Loading history...
288
            float,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
289
            str,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
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
Wrong hanging indentation before block (add 4 spaces).
Loading history...
315
                and argument_type is not int
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
316
                and argument_type is not float
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
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
Wrong hanging indentation before block (add 4 spaces).
Loading history...
358
    *,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
359
    name: Optional[str] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
360
    enable_default: Optional[bool] = True,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
361
    guild: Union[Snowflake, int, str] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
362
    cooldown: Optional[int] = 0,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
363
    cooldown_scale: Optional[float] = 60,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
364
    cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
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
Wrong hanging indentation before block (add 4 spaces).
Loading history...
453
    *,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
454
    name: Optional[str] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
455
    enable_default: Optional[bool] = True,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
456
    guild: Union[Snowflake, int, str] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
457
    cooldown: Optional[int] = 0,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
458
    cooldown_scale: Optional[float] = 60,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
459
    cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
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
Missing function or method docstring
Loading history...
Too many arguments (11/5)
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (17/15).
Loading history...
542
    func: Callable[..., Any] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
543
    app_command_type: Optional[AppCommandType] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
544
    name: Optional[str] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
545
    description: Optional[str] = MISSING,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
546
    enable_default: Optional[bool] = True,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
547
    guild: Optional[Union[Snowflake, int, str]] = None,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
548
    cooldown: Optional[int] = 0,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
549
    cooldown_scale: Optional[float] = 60.0,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
550
    cooldown_scope: Optional[ThrottleScope] = ThrottleScope.USER,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
551
    command_options=MISSING,  # Missing typehint?
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
552
    parent: Optional[Union[Group, Subgroup]] = MISSING,
0 ignored issues
show
Wrong hanging indentation before block (add 4 spaces).
Loading history...
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
Wrong hanging indentation before block (add 4 spaces).
Loading history...
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
Use lazy % formatting in logging functions
Loading history...
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