disagreement 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- disagreement/__init__.py +1 -1
- disagreement/ext/__init__.py +0 -0
- disagreement/ext/app_commands/__init__.py +46 -0
- disagreement/ext/app_commands/commands.py +513 -0
- disagreement/ext/app_commands/context.py +556 -0
- disagreement/ext/app_commands/converters.py +478 -0
- disagreement/ext/app_commands/decorators.py +569 -0
- disagreement/ext/app_commands/handler.py +627 -0
- disagreement/ext/commands/__init__.py +49 -0
- disagreement/ext/commands/cog.py +155 -0
- disagreement/ext/commands/converters.py +175 -0
- disagreement/ext/commands/core.py +490 -0
- disagreement/ext/commands/decorators.py +150 -0
- disagreement/ext/commands/errors.py +76 -0
- disagreement/ext/commands/help.py +37 -0
- disagreement/ext/commands/view.py +103 -0
- disagreement/ext/loader.py +43 -0
- disagreement/ext/tasks.py +89 -0
- disagreement/gateway.py +11 -8
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/METADATA +39 -32
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/RECORD +24 -7
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/WHEEL +0 -0
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.0.1.dist-info → disagreement-0.0.2.dist-info}/top_level.txt +0 -0
disagreement/__init__.py
CHANGED
@@ -14,7 +14,7 @@ __title__ = "disagreement"
|
|
14
14
|
__author__ = "Slipstream"
|
15
15
|
__license__ = "BSD 3-Clause License"
|
16
16
|
__copyright__ = "Copyright 2025 Slipstream"
|
17
|
-
__version__ = "0.0.
|
17
|
+
__version__ = "0.0.2"
|
18
18
|
|
19
19
|
from .client import Client
|
20
20
|
from .models import Message, User
|
File without changes
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# disagreement/ext/app_commands/__init__.py
|
2
|
+
|
3
|
+
"""
|
4
|
+
Application Commands Extension for Disagreement.
|
5
|
+
|
6
|
+
This package provides the framework for creating and handling
|
7
|
+
Discord Application Commands (slash commands, user commands, message commands).
|
8
|
+
"""
|
9
|
+
|
10
|
+
from .commands import (
|
11
|
+
AppCommand,
|
12
|
+
SlashCommand,
|
13
|
+
UserCommand,
|
14
|
+
MessageCommand,
|
15
|
+
AppCommandGroup,
|
16
|
+
)
|
17
|
+
from .decorators import (
|
18
|
+
slash_command,
|
19
|
+
user_command,
|
20
|
+
message_command,
|
21
|
+
hybrid_command,
|
22
|
+
group,
|
23
|
+
subcommand,
|
24
|
+
subcommand_group,
|
25
|
+
OptionMetadata,
|
26
|
+
)
|
27
|
+
from .context import AppCommandContext
|
28
|
+
|
29
|
+
# from .handler import AppCommandHandler # Will be imported when defined
|
30
|
+
|
31
|
+
__all__ = [
|
32
|
+
"AppCommand",
|
33
|
+
"SlashCommand",
|
34
|
+
"UserCommand",
|
35
|
+
"MessageCommand",
|
36
|
+
"AppCommandGroup", # To be defined
|
37
|
+
"slash_command",
|
38
|
+
"user_command",
|
39
|
+
"message_command",
|
40
|
+
"hybrid_command",
|
41
|
+
"group",
|
42
|
+
"subcommand",
|
43
|
+
"subcommand_group",
|
44
|
+
"OptionMetadata",
|
45
|
+
"AppCommandContext", # To be defined
|
46
|
+
]
|
@@ -0,0 +1,513 @@
|
|
1
|
+
# disagreement/ext/app_commands/commands.py
|
2
|
+
|
3
|
+
import inspect
|
4
|
+
from typing import Callable, Optional, List, Dict, Any, Union, TYPE_CHECKING
|
5
|
+
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from disagreement.ext.commands.core import (
|
9
|
+
Command as PrefixCommand,
|
10
|
+
) # Alias to avoid name clash
|
11
|
+
from disagreement.interactions import ApplicationCommandOption, Snowflake
|
12
|
+
from disagreement.enums import (
|
13
|
+
ApplicationCommandType,
|
14
|
+
IntegrationType,
|
15
|
+
InteractionContextType,
|
16
|
+
ApplicationCommandOptionType, # Added
|
17
|
+
)
|
18
|
+
from disagreement.ext.commands.cog import Cog # Corrected import path
|
19
|
+
|
20
|
+
# Placeholder for Cog if not using the existing one or if it needs adaptation
|
21
|
+
if not TYPE_CHECKING:
|
22
|
+
# This dynamic Cog = Any might not be ideal if Cog is used in runtime type checks.
|
23
|
+
# However, for type hinting purposes when TYPE_CHECKING is false, it avoids import.
|
24
|
+
# If Cog is needed at runtime by this module (it is, for AppCommand.cog type hint),
|
25
|
+
# it should be imported directly.
|
26
|
+
# For now, the TYPE_CHECKING block handles the proper import for static analysis.
|
27
|
+
# Let's ensure Cog is available at runtime if AppCommand.cog is accessed.
|
28
|
+
# A simple way is to import it outside TYPE_CHECKING too, if it doesn't cause circularity.
|
29
|
+
# Given its usage, a forward reference string 'Cog' might be better in AppCommand.cog type hint.
|
30
|
+
# Let's try importing it directly for runtime, assuming no circularity with this specific module.
|
31
|
+
try:
|
32
|
+
from disagreement.ext.commands.cog import Cog
|
33
|
+
except ImportError:
|
34
|
+
Cog = Any # Fallback if direct import fails (e.g. during partial builds/tests)
|
35
|
+
# Import PrefixCommand at runtime for HybridCommand
|
36
|
+
try:
|
37
|
+
from disagreement.ext.commands.core import Command as PrefixCommand
|
38
|
+
except Exception: # pragma: no cover - safeguard against unusual import issues
|
39
|
+
PrefixCommand = Any # type: ignore
|
40
|
+
# Import enums used at runtime
|
41
|
+
try:
|
42
|
+
from disagreement.enums import (
|
43
|
+
ApplicationCommandType,
|
44
|
+
IntegrationType,
|
45
|
+
InteractionContextType,
|
46
|
+
ApplicationCommandOptionType,
|
47
|
+
)
|
48
|
+
from disagreement.interactions import ApplicationCommandOption, Snowflake
|
49
|
+
except Exception: # pragma: no cover
|
50
|
+
ApplicationCommandType = ApplicationCommandOptionType = IntegrationType = (
|
51
|
+
InteractionContextType
|
52
|
+
) = Any # type: ignore
|
53
|
+
ApplicationCommandOption = Snowflake = Any # type: ignore
|
54
|
+
else: # When TYPE_CHECKING is true, Cog and PrefixCommand are already imported above.
|
55
|
+
pass
|
56
|
+
|
57
|
+
|
58
|
+
class AppCommand:
|
59
|
+
"""
|
60
|
+
Base class for an application command.
|
61
|
+
|
62
|
+
Attributes:
|
63
|
+
name (str): The name of the command.
|
64
|
+
description (Optional[str]): The description of the command.
|
65
|
+
Required for CHAT_INPUT, empty for USER and MESSAGE commands.
|
66
|
+
callback (Callable[..., Any]): The coroutine function that will be called when the command is executed.
|
67
|
+
type (ApplicationCommandType): The type of the application command.
|
68
|
+
options (Optional[List[ApplicationCommandOption]]): Parameters for the command. Populated by decorators.
|
69
|
+
guild_ids (Optional[List[Snowflake]]): List of guild IDs where this command is active. None for global.
|
70
|
+
default_member_permissions (Optional[str]): Bitwise permissions required by default for users to run the command.
|
71
|
+
nsfw (bool): Whether the command is age-restricted.
|
72
|
+
parent (Optional['AppCommandGroup']): The parent group if this is a subcommand.
|
73
|
+
cog (Optional[Cog]): The cog this command belongs to, if any.
|
74
|
+
_full_description (Optional[str]): Stores the original full description, e.g. from docstring,
|
75
|
+
even if the payload description is different (like for User/Message commands).
|
76
|
+
name_localizations (Optional[Dict[str, str]]): Localizations for the command's name.
|
77
|
+
description_localizations (Optional[Dict[str, str]]): Localizations for the command's description.
|
78
|
+
integration_types (Optional[List[IntegrationType]]): Installation contexts.
|
79
|
+
contexts (Optional[List[InteractionContextType]]): Interaction contexts.
|
80
|
+
"""
|
81
|
+
|
82
|
+
def __init__(
|
83
|
+
self,
|
84
|
+
callback: Callable[..., Any],
|
85
|
+
*,
|
86
|
+
name: str,
|
87
|
+
description: Optional[str] = None,
|
88
|
+
locale: Optional[str] = None,
|
89
|
+
type: "ApplicationCommandType",
|
90
|
+
guild_ids: Optional[List["Snowflake"]] = None,
|
91
|
+
default_member_permissions: Optional[str] = None,
|
92
|
+
nsfw: bool = False,
|
93
|
+
parent: Optional["AppCommandGroup"] = None,
|
94
|
+
cog: Optional[
|
95
|
+
Any
|
96
|
+
] = None, # Changed 'Cog' to Any to avoid runtime import issues if Cog is complex
|
97
|
+
name_localizations: Optional[Dict[str, str]] = None,
|
98
|
+
description_localizations: Optional[Dict[str, str]] = None,
|
99
|
+
integration_types: Optional[List["IntegrationType"]] = None,
|
100
|
+
contexts: Optional[List["InteractionContextType"]] = None,
|
101
|
+
):
|
102
|
+
if not asyncio.iscoroutinefunction(callback):
|
103
|
+
raise TypeError(
|
104
|
+
"Application command callback must be a coroutine function."
|
105
|
+
)
|
106
|
+
|
107
|
+
if locale:
|
108
|
+
from disagreement import i18n
|
109
|
+
|
110
|
+
translate = i18n.translate
|
111
|
+
|
112
|
+
self.name = translate(name, locale)
|
113
|
+
self.description = (
|
114
|
+
translate(description, locale) if description is not None else None
|
115
|
+
)
|
116
|
+
else:
|
117
|
+
self.name = name
|
118
|
+
self.description = description
|
119
|
+
self.locale: Optional[str] = locale
|
120
|
+
self.callback: Callable[..., Any] = callback
|
121
|
+
self.type: "ApplicationCommandType" = type
|
122
|
+
self.options: List["ApplicationCommandOption"] = [] # Populated by decorator
|
123
|
+
self.guild_ids: Optional[List["Snowflake"]] = guild_ids
|
124
|
+
self.default_member_permissions: Optional[str] = default_member_permissions
|
125
|
+
self.nsfw: bool = nsfw
|
126
|
+
self.parent: Optional["AppCommandGroup"] = parent
|
127
|
+
self.cog: Optional[Any] = cog # Changed 'Cog' to Any
|
128
|
+
self.name_localizations: Optional[Dict[str, str]] = name_localizations
|
129
|
+
self.description_localizations: Optional[Dict[str, str]] = (
|
130
|
+
description_localizations
|
131
|
+
)
|
132
|
+
self.integration_types: Optional[List["IntegrationType"]] = integration_types
|
133
|
+
self.contexts: Optional[List["InteractionContextType"]] = contexts
|
134
|
+
self._full_description: Optional[str] = (
|
135
|
+
None # Initialized by decorator if needed
|
136
|
+
)
|
137
|
+
|
138
|
+
# Signature for argument parsing by decorators/handlers
|
139
|
+
self.params = inspect.signature(callback).parameters
|
140
|
+
|
141
|
+
async def invoke(
|
142
|
+
self, context: "AppCommandContext", *args: Any, **kwargs: Any
|
143
|
+
) -> None:
|
144
|
+
"""Invokes the command's callback with the given context and arguments."""
|
145
|
+
# Similar to Command.invoke, handle cog if present
|
146
|
+
actual_args = []
|
147
|
+
if self.cog:
|
148
|
+
actual_args.append(self.cog)
|
149
|
+
actual_args.append(context)
|
150
|
+
actual_args.extend(args)
|
151
|
+
|
152
|
+
await self.callback(*actual_args, **kwargs)
|
153
|
+
|
154
|
+
def to_dict(self) -> Dict[str, Any]:
|
155
|
+
"""Converts the command to a dictionary payload for Discord API."""
|
156
|
+
payload: Dict[str, Any] = {
|
157
|
+
"name": self.name,
|
158
|
+
"type": self.type.value,
|
159
|
+
# CHAT_INPUT commands require a description.
|
160
|
+
# USER and MESSAGE commands must have an empty description in the payload if not omitted.
|
161
|
+
# The constructor for UserCommand/MessageCommand already sets self.description to ""
|
162
|
+
"description": (
|
163
|
+
self.description
|
164
|
+
if self.type == ApplicationCommandType.CHAT_INPUT
|
165
|
+
else ""
|
166
|
+
),
|
167
|
+
}
|
168
|
+
|
169
|
+
# For CHAT_INPUT commands, options are its parameters.
|
170
|
+
# For USER/MESSAGE commands, options should be empty or not present.
|
171
|
+
if self.type == ApplicationCommandType.CHAT_INPUT and self.options:
|
172
|
+
payload["options"] = [opt.to_dict() for opt in self.options]
|
173
|
+
|
174
|
+
if self.default_member_permissions is not None: # Can be "0" for no permissions
|
175
|
+
payload["default_member_permissions"] = str(self.default_member_permissions)
|
176
|
+
|
177
|
+
# nsfw defaults to False, only include if True
|
178
|
+
if self.nsfw:
|
179
|
+
payload["nsfw"] = True
|
180
|
+
|
181
|
+
if self.name_localizations:
|
182
|
+
payload["name_localizations"] = self.name_localizations
|
183
|
+
|
184
|
+
# Description localizations only apply if there's a description (CHAT_INPUT commands)
|
185
|
+
if (
|
186
|
+
self.type == ApplicationCommandType.CHAT_INPUT
|
187
|
+
and self.description
|
188
|
+
and self.description_localizations
|
189
|
+
):
|
190
|
+
payload["description_localizations"] = self.description_localizations
|
191
|
+
|
192
|
+
if self.integration_types:
|
193
|
+
payload["integration_types"] = [it.value for it in self.integration_types]
|
194
|
+
|
195
|
+
if self.contexts:
|
196
|
+
payload["contexts"] = [ict.value for ict in self.contexts]
|
197
|
+
|
198
|
+
# According to Discord API, guild_id is not part of this payload,
|
199
|
+
# it's used in the URL path for guild-specific command registration.
|
200
|
+
# However, the global command registration takes an 'application_id' in the payload,
|
201
|
+
# but that's handled by the HTTPClient.
|
202
|
+
|
203
|
+
return payload
|
204
|
+
|
205
|
+
def __repr__(self) -> str:
|
206
|
+
return f"<{self.__class__.__name__} name='{self.name}' type={self.type!r}>"
|
207
|
+
|
208
|
+
|
209
|
+
class SlashCommand(AppCommand):
|
210
|
+
"""Represents a CHAT_INPUT (slash) command."""
|
211
|
+
|
212
|
+
def __init__(self, callback: Callable[..., Any], **kwargs: Any):
|
213
|
+
if not kwargs.get("description"):
|
214
|
+
raise ValueError("SlashCommand requires a description.")
|
215
|
+
super().__init__(callback, type=ApplicationCommandType.CHAT_INPUT, **kwargs)
|
216
|
+
|
217
|
+
|
218
|
+
class UserCommand(AppCommand):
|
219
|
+
"""Represents a USER context menu command."""
|
220
|
+
|
221
|
+
def __init__(self, callback: Callable[..., Any], **kwargs: Any):
|
222
|
+
# Description is not allowed by Discord API for User Commands, but can be set to empty string.
|
223
|
+
kwargs["description"] = kwargs.get(
|
224
|
+
"description", ""
|
225
|
+
) # Ensure it's empty or not present in payload
|
226
|
+
super().__init__(callback, type=ApplicationCommandType.USER, **kwargs)
|
227
|
+
|
228
|
+
|
229
|
+
class MessageCommand(AppCommand):
|
230
|
+
"""Represents a MESSAGE context menu command."""
|
231
|
+
|
232
|
+
def __init__(self, callback: Callable[..., Any], **kwargs: Any):
|
233
|
+
# Description is not allowed by Discord API for Message Commands.
|
234
|
+
kwargs["description"] = kwargs.get("description", "")
|
235
|
+
super().__init__(callback, type=ApplicationCommandType.MESSAGE, **kwargs)
|
236
|
+
|
237
|
+
|
238
|
+
class HybridCommand(SlashCommand, PrefixCommand): # Inherit from both
|
239
|
+
"""
|
240
|
+
Represents a command that can be invoked as both a slash command
|
241
|
+
and a traditional prefix-based command.
|
242
|
+
"""
|
243
|
+
|
244
|
+
def __init__(self, callback: Callable[..., Any], **kwargs: Any):
|
245
|
+
# Initialize SlashCommand part (which calls AppCommand.__init__)
|
246
|
+
# We need to ensure 'type' is correctly passed for AppCommand
|
247
|
+
# kwargs for SlashCommand: name, description, guild_ids, default_member_permissions, nsfw, parent, cog, etc.
|
248
|
+
# kwargs for PrefixCommand: name, aliases, brief, description, cog
|
249
|
+
|
250
|
+
# Pop prefix-specific args before passing to SlashCommand constructor
|
251
|
+
prefix_aliases = kwargs.pop("aliases", [])
|
252
|
+
prefix_brief = kwargs.pop("brief", None)
|
253
|
+
# Description is used by both, AppCommand's constructor will handle it.
|
254
|
+
# Name is used by both. Cog is used by both.
|
255
|
+
|
256
|
+
# Call SlashCommand's __init__
|
257
|
+
# This will set up name, description, callback, type=CHAT_INPUT, options, etc.
|
258
|
+
super().__init__(callback, **kwargs) # This is SlashCommand.__init__
|
259
|
+
|
260
|
+
# Now, explicitly initialize the PrefixCommand parts that SlashCommand didn't cover
|
261
|
+
# or that need specific values for the prefix version.
|
262
|
+
# PrefixCommand.__init__(self, callback, name=self.name, aliases=prefix_aliases, brief=prefix_brief, description=self.description, cog=self.cog)
|
263
|
+
# However, PrefixCommand.__init__ also sets self.params, which AppCommand already did.
|
264
|
+
# We need to be careful not to re-initialize things unnecessarily or incorrectly.
|
265
|
+
# Let's manually set the distinct attributes for the PrefixCommand aspect.
|
266
|
+
|
267
|
+
# Attributes from PrefixCommand:
|
268
|
+
# self.callback is already set by AppCommand
|
269
|
+
# self.name is already set by AppCommand
|
270
|
+
self.aliases: List[str] = (
|
271
|
+
prefix_aliases # This was specific to HybridCommand before, now aligns with PrefixCommand
|
272
|
+
)
|
273
|
+
self.brief: Optional[str] = prefix_brief
|
274
|
+
# self.description is already set by AppCommand (SlashCommand ensures it exists)
|
275
|
+
# self.cog is already set by AppCommand
|
276
|
+
# self.params is already set by AppCommand
|
277
|
+
|
278
|
+
# Ensure the MRO is handled correctly. Python's MRO (C3 linearization)
|
279
|
+
# should call SlashCommand's __init__ then AppCommand's __init__.
|
280
|
+
# PrefixCommand.__init__ won't be called automatically unless we explicitly call it.
|
281
|
+
# By setting attributes directly, we avoid potential issues with multiple __init__ calls
|
282
|
+
# if their logic overlaps too much (e.g., both trying to set self.params).
|
283
|
+
|
284
|
+
# We might need to override invoke if the context or argument passing differs significantly
|
285
|
+
# between app command invocation and prefix command invocation.
|
286
|
+
# For now, SlashCommand.invoke and PrefixCommand.invoke are separate.
|
287
|
+
# The correct one will be called depending on how the command is dispatched.
|
288
|
+
# The AppCommandHandler will use AppCommand.invoke (via SlashCommand).
|
289
|
+
# The prefix CommandHandler will use PrefixCommand.invoke.
|
290
|
+
# This seems acceptable.
|
291
|
+
|
292
|
+
|
293
|
+
class AppCommandGroup:
|
294
|
+
"""
|
295
|
+
Represents a group of application commands (subcommands or subcommand groups).
|
296
|
+
This itself is not directly callable but acts as a namespace.
|
297
|
+
"""
|
298
|
+
|
299
|
+
def __init__(
|
300
|
+
self,
|
301
|
+
name: str,
|
302
|
+
description: Optional[
|
303
|
+
str
|
304
|
+
] = None, # Required for top-level groups that form part of a slash command
|
305
|
+
guild_ids: Optional[List["Snowflake"]] = None,
|
306
|
+
parent: Optional["AppCommandGroup"] = None,
|
307
|
+
default_member_permissions: Optional[str] = None,
|
308
|
+
nsfw: bool = False,
|
309
|
+
name_localizations: Optional[Dict[str, str]] = None,
|
310
|
+
description_localizations: Optional[Dict[str, str]] = None,
|
311
|
+
integration_types: Optional[List["IntegrationType"]] = None,
|
312
|
+
contexts: Optional[List["InteractionContextType"]] = None,
|
313
|
+
):
|
314
|
+
self.name: str = name
|
315
|
+
self.description: Optional[str] = description
|
316
|
+
self.guild_ids: Optional[List["Snowflake"]] = guild_ids
|
317
|
+
self.parent: Optional["AppCommandGroup"] = parent
|
318
|
+
self.commands: Dict[str, Union[AppCommand, "AppCommandGroup"]] = {}
|
319
|
+
self.default_member_permissions: Optional[str] = default_member_permissions
|
320
|
+
self.nsfw: bool = nsfw
|
321
|
+
self.name_localizations: Optional[Dict[str, str]] = name_localizations
|
322
|
+
self.description_localizations: Optional[Dict[str, str]] = (
|
323
|
+
description_localizations
|
324
|
+
)
|
325
|
+
self.integration_types: Optional[List["IntegrationType"]] = integration_types
|
326
|
+
self.contexts: Optional[List["InteractionContextType"]] = contexts
|
327
|
+
# A group itself doesn't have a cog directly, its commands do.
|
328
|
+
|
329
|
+
def add_command(self, command: Union[AppCommand, "AppCommandGroup"]) -> None:
|
330
|
+
if command.name in self.commands:
|
331
|
+
raise ValueError(
|
332
|
+
f"Command or group '{command.name}' already exists in group '{self.name}'."
|
333
|
+
)
|
334
|
+
command.parent = self
|
335
|
+
self.commands[command.name] = command
|
336
|
+
|
337
|
+
def get_command(self, name: str) -> Optional[Union[AppCommand, "AppCommandGroup"]]:
|
338
|
+
return self.commands.get(name)
|
339
|
+
|
340
|
+
def command(self, *d_args: Any, **d_kwargs: Any):
|
341
|
+
d_kwargs.setdefault("parent", self)
|
342
|
+
from .decorators import slash_command
|
343
|
+
|
344
|
+
return slash_command(*d_args, **d_kwargs)
|
345
|
+
|
346
|
+
def group(
|
347
|
+
self,
|
348
|
+
name: str,
|
349
|
+
description: Optional[str] = None,
|
350
|
+
**kwargs: Any,
|
351
|
+
):
|
352
|
+
sub_group = AppCommandGroup(
|
353
|
+
name=name,
|
354
|
+
description=description,
|
355
|
+
parent=self,
|
356
|
+
guild_ids=kwargs.get("guild_ids"),
|
357
|
+
default_member_permissions=kwargs.get("default_member_permissions"),
|
358
|
+
nsfw=kwargs.get("nsfw", False),
|
359
|
+
name_localizations=kwargs.get("name_localizations"),
|
360
|
+
description_localizations=kwargs.get("description_localizations"),
|
361
|
+
integration_types=kwargs.get("integration_types"),
|
362
|
+
contexts=kwargs.get("contexts"),
|
363
|
+
)
|
364
|
+
self.add_command(sub_group)
|
365
|
+
|
366
|
+
def decorator(func: Optional[Callable[..., Any]] = None):
|
367
|
+
if func is not None:
|
368
|
+
setattr(func, "__app_command_object__", sub_group)
|
369
|
+
return sub_group
|
370
|
+
return sub_group
|
371
|
+
|
372
|
+
return decorator
|
373
|
+
|
374
|
+
def __repr__(self) -> str:
|
375
|
+
return f"<AppCommandGroup name='{self.name}' commands={len(self.commands)}>"
|
376
|
+
|
377
|
+
def to_dict(self) -> Dict[str, Any]:
|
378
|
+
"""
|
379
|
+
Converts the command group to a dictionary payload for Discord API.
|
380
|
+
This represents a top-level command that has subcommands/subcommand groups.
|
381
|
+
"""
|
382
|
+
payload: Dict[str, Any] = {
|
383
|
+
"name": self.name,
|
384
|
+
"type": ApplicationCommandType.CHAT_INPUT.value, # Groups are implicitly CHAT_INPUT
|
385
|
+
"description": self.description
|
386
|
+
or "No description provided", # Top-level groups require a description
|
387
|
+
"options": [],
|
388
|
+
}
|
389
|
+
|
390
|
+
if self.default_member_permissions is not None:
|
391
|
+
payload["default_member_permissions"] = str(self.default_member_permissions)
|
392
|
+
if self.nsfw:
|
393
|
+
payload["nsfw"] = True
|
394
|
+
if self.name_localizations:
|
395
|
+
payload["name_localizations"] = self.name_localizations
|
396
|
+
if (
|
397
|
+
self.description and self.description_localizations
|
398
|
+
): # Only if description is not empty
|
399
|
+
payload["description_localizations"] = self.description_localizations
|
400
|
+
if self.integration_types:
|
401
|
+
payload["integration_types"] = [it.value for it in self.integration_types]
|
402
|
+
if self.contexts:
|
403
|
+
payload["contexts"] = [ict.value for ict in self.contexts]
|
404
|
+
|
405
|
+
# guild_ids are handled at the registration level, not in this specific payload part.
|
406
|
+
|
407
|
+
options_payload: List[Dict[str, Any]] = []
|
408
|
+
for cmd_name, command_or_group in self.commands.items():
|
409
|
+
if isinstance(command_or_group, AppCommand): # This is a Subcommand
|
410
|
+
# Subcommands use their own options (parameters)
|
411
|
+
sub_options = (
|
412
|
+
[opt.to_dict() for opt in command_or_group.options]
|
413
|
+
if command_or_group.options
|
414
|
+
else []
|
415
|
+
)
|
416
|
+
option_dict = {
|
417
|
+
"type": ApplicationCommandOptionType.SUB_COMMAND.value,
|
418
|
+
"name": command_or_group.name,
|
419
|
+
"description": command_or_group.description
|
420
|
+
or "No description provided",
|
421
|
+
"options": sub_options,
|
422
|
+
}
|
423
|
+
# Add localization for subcommand name and description if available
|
424
|
+
if command_or_group.name_localizations:
|
425
|
+
option_dict["name_localizations"] = (
|
426
|
+
command_or_group.name_localizations
|
427
|
+
)
|
428
|
+
if (
|
429
|
+
command_or_group.description
|
430
|
+
and command_or_group.description_localizations
|
431
|
+
):
|
432
|
+
option_dict["description_localizations"] = (
|
433
|
+
command_or_group.description_localizations
|
434
|
+
)
|
435
|
+
options_payload.append(option_dict)
|
436
|
+
|
437
|
+
elif isinstance(
|
438
|
+
command_or_group, AppCommandGroup
|
439
|
+
): # This is a Subcommand Group
|
440
|
+
# Subcommand groups have their subcommands/groups as options
|
441
|
+
sub_group_options: List[Dict[str, Any]] = []
|
442
|
+
for sub_cmd_name, sub_command in command_or_group.commands.items():
|
443
|
+
# Nested groups can only contain subcommands, not further nested groups as per Discord rules.
|
444
|
+
# So, sub_command here must be an AppCommand.
|
445
|
+
if isinstance(
|
446
|
+
sub_command, AppCommand
|
447
|
+
): # Should always be AppCommand if structure is valid
|
448
|
+
sub_cmd_options = (
|
449
|
+
[opt.to_dict() for opt in sub_command.options]
|
450
|
+
if sub_command.options
|
451
|
+
else []
|
452
|
+
)
|
453
|
+
sub_group_option_entry = {
|
454
|
+
"type": ApplicationCommandOptionType.SUB_COMMAND.value,
|
455
|
+
"name": sub_command.name,
|
456
|
+
"description": sub_command.description
|
457
|
+
or "No description provided",
|
458
|
+
"options": sub_cmd_options,
|
459
|
+
}
|
460
|
+
# Add localization for subcommand name and description if available
|
461
|
+
if sub_command.name_localizations:
|
462
|
+
sub_group_option_entry["name_localizations"] = (
|
463
|
+
sub_command.name_localizations
|
464
|
+
)
|
465
|
+
if (
|
466
|
+
sub_command.description
|
467
|
+
and sub_command.description_localizations
|
468
|
+
):
|
469
|
+
sub_group_option_entry["description_localizations"] = (
|
470
|
+
sub_command.description_localizations
|
471
|
+
)
|
472
|
+
sub_group_options.append(sub_group_option_entry)
|
473
|
+
# else:
|
474
|
+
# # This case implies a group nested inside a group, which then contains another group.
|
475
|
+
# # Discord's structure is:
|
476
|
+
# # command -> option (SUB_COMMAND_GROUP) -> option (SUB_COMMAND) -> option (param)
|
477
|
+
# # This should be caught by validation logic in decorators or add_command.
|
478
|
+
# # For now, we assume valid structure where AppCommandGroup's commands are AppCommands.
|
479
|
+
# pass
|
480
|
+
|
481
|
+
option_dict = {
|
482
|
+
"type": ApplicationCommandOptionType.SUB_COMMAND_GROUP.value,
|
483
|
+
"name": command_or_group.name,
|
484
|
+
"description": command_or_group.description
|
485
|
+
or "No description provided",
|
486
|
+
"options": sub_group_options, # These are the SUB_COMMANDs
|
487
|
+
}
|
488
|
+
# Add localization for subcommand group name and description if available
|
489
|
+
if command_or_group.name_localizations:
|
490
|
+
option_dict["name_localizations"] = (
|
491
|
+
command_or_group.name_localizations
|
492
|
+
)
|
493
|
+
if (
|
494
|
+
command_or_group.description
|
495
|
+
and command_or_group.description_localizations
|
496
|
+
):
|
497
|
+
option_dict["description_localizations"] = (
|
498
|
+
command_or_group.description_localizations
|
499
|
+
)
|
500
|
+
options_payload.append(option_dict)
|
501
|
+
|
502
|
+
payload["options"] = options_payload
|
503
|
+
return payload
|
504
|
+
|
505
|
+
|
506
|
+
# Need to import asyncio for iscoroutinefunction check
|
507
|
+
import asyncio
|
508
|
+
|
509
|
+
if TYPE_CHECKING:
|
510
|
+
from .context import AppCommandContext # For type hint in AppCommand.invoke
|
511
|
+
|
512
|
+
# Ensure ApplicationCommandOptionType is available for the to_dict method
|
513
|
+
from disagreement.enums import ApplicationCommandOptionType
|