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
@@ -0,0 +1,627 @@
|
|
1
|
+
# disagreement/ext/app_commands/handler.py
|
2
|
+
|
3
|
+
import inspect
|
4
|
+
from typing import (
|
5
|
+
TYPE_CHECKING,
|
6
|
+
Dict,
|
7
|
+
Optional,
|
8
|
+
List,
|
9
|
+
Any,
|
10
|
+
Tuple,
|
11
|
+
Union,
|
12
|
+
get_origin,
|
13
|
+
get_args,
|
14
|
+
Literal,
|
15
|
+
)
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from disagreement.client import Client
|
19
|
+
from disagreement.interactions import Interaction, ResolvedData, Snowflake
|
20
|
+
from disagreement.enums import (
|
21
|
+
ApplicationCommandType,
|
22
|
+
ApplicationCommandOptionType,
|
23
|
+
InteractionType,
|
24
|
+
)
|
25
|
+
from .commands import (
|
26
|
+
AppCommand,
|
27
|
+
SlashCommand,
|
28
|
+
UserCommand,
|
29
|
+
MessageCommand,
|
30
|
+
AppCommandGroup,
|
31
|
+
)
|
32
|
+
from .context import AppCommandContext
|
33
|
+
from disagreement.models import (
|
34
|
+
User,
|
35
|
+
Member,
|
36
|
+
Role,
|
37
|
+
Attachment,
|
38
|
+
Message,
|
39
|
+
) # For resolved data
|
40
|
+
|
41
|
+
# Channel models would also go here
|
42
|
+
|
43
|
+
# Placeholder for models not yet fully defined or imported
|
44
|
+
if not TYPE_CHECKING:
|
45
|
+
from disagreement.enums import (
|
46
|
+
ApplicationCommandType,
|
47
|
+
ApplicationCommandOptionType,
|
48
|
+
InteractionType,
|
49
|
+
)
|
50
|
+
from .commands import (
|
51
|
+
AppCommand,
|
52
|
+
SlashCommand,
|
53
|
+
UserCommand,
|
54
|
+
MessageCommand,
|
55
|
+
AppCommandGroup,
|
56
|
+
)
|
57
|
+
from .context import AppCommandContext
|
58
|
+
|
59
|
+
User = Any
|
60
|
+
Member = Any
|
61
|
+
Role = Any
|
62
|
+
Attachment = Any
|
63
|
+
Channel = Any
|
64
|
+
Message = Any
|
65
|
+
|
66
|
+
|
67
|
+
class AppCommandHandler:
|
68
|
+
"""
|
69
|
+
Manages application command registration, parsing, and dispatching.
|
70
|
+
"""
|
71
|
+
|
72
|
+
def __init__(self, client: "Client"):
|
73
|
+
self.client: "Client" = client
|
74
|
+
# Store commands: key could be (name, type) for global, or (name, type, guild_id) for guild-specific
|
75
|
+
# For simplicity, let's start with a flat structure and refine if needed for guild commands.
|
76
|
+
# A more robust system might have separate dicts for global and guild commands.
|
77
|
+
self._slash_commands: Dict[str, SlashCommand] = {}
|
78
|
+
self._user_commands: Dict[str, UserCommand] = {}
|
79
|
+
self._message_commands: Dict[str, MessageCommand] = {}
|
80
|
+
self._app_command_groups: Dict[str, AppCommandGroup] = {}
|
81
|
+
self._converter_registry: Dict[type, type] = {}
|
82
|
+
|
83
|
+
def add_command(self, command: Union["AppCommand", "AppCommandGroup"]) -> None:
|
84
|
+
"""Adds an application command or a command group to the handler."""
|
85
|
+
if isinstance(command, AppCommandGroup):
|
86
|
+
if command.name in self._app_command_groups:
|
87
|
+
raise ValueError(
|
88
|
+
f"AppCommandGroup '{command.name}' is already registered."
|
89
|
+
)
|
90
|
+
self._app_command_groups[command.name] = command
|
91
|
+
return
|
92
|
+
|
93
|
+
if isinstance(command, SlashCommand):
|
94
|
+
if command.name in self._slash_commands:
|
95
|
+
raise ValueError(
|
96
|
+
f"SlashCommand '{command.name}' is already registered."
|
97
|
+
)
|
98
|
+
self._slash_commands[command.name] = command
|
99
|
+
return
|
100
|
+
|
101
|
+
if isinstance(command, UserCommand):
|
102
|
+
if command.name in self._user_commands:
|
103
|
+
raise ValueError(f"UserCommand '{command.name}' is already registered.")
|
104
|
+
self._user_commands[command.name] = command
|
105
|
+
return
|
106
|
+
|
107
|
+
if isinstance(command, MessageCommand):
|
108
|
+
if command.name in self._message_commands:
|
109
|
+
raise ValueError(
|
110
|
+
f"MessageCommand '{command.name}' is already registered."
|
111
|
+
)
|
112
|
+
self._message_commands[command.name] = command
|
113
|
+
return
|
114
|
+
|
115
|
+
if isinstance(command, AppCommand):
|
116
|
+
# Fallback for plain AppCommand objects
|
117
|
+
if command.type == ApplicationCommandType.CHAT_INPUT:
|
118
|
+
if command.name in self._slash_commands:
|
119
|
+
raise ValueError(
|
120
|
+
f"SlashCommand '{command.name}' is already registered."
|
121
|
+
)
|
122
|
+
self._slash_commands[command.name] = command # type: ignore
|
123
|
+
elif command.type == ApplicationCommandType.USER:
|
124
|
+
if command.name in self._user_commands:
|
125
|
+
raise ValueError(
|
126
|
+
f"UserCommand '{command.name}' is already registered."
|
127
|
+
)
|
128
|
+
self._user_commands[command.name] = command # type: ignore
|
129
|
+
elif command.type == ApplicationCommandType.MESSAGE:
|
130
|
+
if command.name in self._message_commands:
|
131
|
+
raise ValueError(
|
132
|
+
f"MessageCommand '{command.name}' is already registered."
|
133
|
+
)
|
134
|
+
self._message_commands[command.name] = command # type: ignore
|
135
|
+
else:
|
136
|
+
raise TypeError(
|
137
|
+
f"Unsupported command type: {command.type} for '{command.name}'"
|
138
|
+
)
|
139
|
+
else:
|
140
|
+
raise TypeError("Can only add AppCommand or AppCommandGroup instances.")
|
141
|
+
|
142
|
+
def remove_command(
|
143
|
+
self, name: str
|
144
|
+
) -> Optional[Union["AppCommand", "AppCommandGroup"]]:
|
145
|
+
"""Removes an application command or group by name."""
|
146
|
+
if name in self._slash_commands:
|
147
|
+
return self._slash_commands.pop(name)
|
148
|
+
if name in self._user_commands:
|
149
|
+
return self._user_commands.pop(name)
|
150
|
+
if name in self._message_commands:
|
151
|
+
return self._message_commands.pop(name)
|
152
|
+
if name in self._app_command_groups:
|
153
|
+
return self._app_command_groups.pop(name)
|
154
|
+
return None
|
155
|
+
|
156
|
+
def register_converter(self, annotation: type, converter_cls: type) -> None:
|
157
|
+
"""Register a custom converter class for a type annotation."""
|
158
|
+
self._converter_registry[annotation] = converter_cls
|
159
|
+
|
160
|
+
def get_converter(self, annotation: type) -> Optional[type]:
|
161
|
+
"""Retrieve a registered converter class for a type annotation."""
|
162
|
+
return self._converter_registry.get(annotation)
|
163
|
+
|
164
|
+
def get_command(
|
165
|
+
self,
|
166
|
+
name: str,
|
167
|
+
command_type: "ApplicationCommandType",
|
168
|
+
interaction_options: Optional[List[Dict[str, Any]]] = None,
|
169
|
+
) -> Optional["AppCommand"]:
|
170
|
+
"""Retrieves a command of a specific type."""
|
171
|
+
if command_type == ApplicationCommandType.CHAT_INPUT:
|
172
|
+
if not interaction_options:
|
173
|
+
return self._slash_commands.get(name)
|
174
|
+
|
175
|
+
# Handle subcommands/groups
|
176
|
+
current_options = interaction_options
|
177
|
+
target_command_or_group: Optional[Union[AppCommand, AppCommandGroup]] = (
|
178
|
+
self._app_command_groups.get(name)
|
179
|
+
)
|
180
|
+
|
181
|
+
if not target_command_or_group:
|
182
|
+
return self._slash_commands.get(name)
|
183
|
+
|
184
|
+
final_command: Optional[AppCommand] = None
|
185
|
+
|
186
|
+
while current_options:
|
187
|
+
opt_data = current_options[0]
|
188
|
+
opt_name = opt_data.get("name")
|
189
|
+
opt_type = (
|
190
|
+
ApplicationCommandOptionType(opt_data["type"])
|
191
|
+
if opt_data.get("type")
|
192
|
+
else None
|
193
|
+
)
|
194
|
+
|
195
|
+
if not opt_name or not isinstance(
|
196
|
+
target_command_or_group, AppCommandGroup
|
197
|
+
):
|
198
|
+
break
|
199
|
+
|
200
|
+
next_target = target_command_or_group.get_command(opt_name)
|
201
|
+
|
202
|
+
if isinstance(next_target, AppCommand) and (
|
203
|
+
opt_type == ApplicationCommandOptionType.SUB_COMMAND
|
204
|
+
or not opt_data.get("options")
|
205
|
+
):
|
206
|
+
final_command = next_target
|
207
|
+
break
|
208
|
+
elif (
|
209
|
+
isinstance(next_target, AppCommandGroup)
|
210
|
+
and opt_type == ApplicationCommandOptionType.SUB_COMMAND_GROUP
|
211
|
+
):
|
212
|
+
target_command_or_group = next_target
|
213
|
+
current_options = opt_data.get("options", [])
|
214
|
+
if not current_options:
|
215
|
+
break
|
216
|
+
else:
|
217
|
+
break
|
218
|
+
|
219
|
+
return final_command
|
220
|
+
|
221
|
+
if command_type == ApplicationCommandType.USER:
|
222
|
+
return self._user_commands.get(name)
|
223
|
+
|
224
|
+
if command_type == ApplicationCommandType.MESSAGE:
|
225
|
+
return self._message_commands.get(name)
|
226
|
+
|
227
|
+
return None
|
228
|
+
|
229
|
+
async def _resolve_option_value(
|
230
|
+
self,
|
231
|
+
value: Any,
|
232
|
+
expected_type: Any,
|
233
|
+
resolved_data: Optional["ResolvedData"],
|
234
|
+
guild_id: Optional["Snowflake"],
|
235
|
+
) -> Any:
|
236
|
+
"""
|
237
|
+
Resolves an option value to the expected Python type using resolved_data.
|
238
|
+
"""
|
239
|
+
converter_cls = self.get_converter(expected_type)
|
240
|
+
if converter_cls:
|
241
|
+
try:
|
242
|
+
init_params = inspect.signature(converter_cls.__init__).parameters
|
243
|
+
if "client" in init_params:
|
244
|
+
converter_instance = converter_cls(client=self.client) # type: ignore[arg-type]
|
245
|
+
else:
|
246
|
+
converter_instance = converter_cls()
|
247
|
+
return await converter_instance.convert(None, value) # type: ignore[arg-type]
|
248
|
+
except Exception:
|
249
|
+
pass
|
250
|
+
|
251
|
+
# This is a simplified resolver. A more robust one would use converters.
|
252
|
+
if resolved_data:
|
253
|
+
if expected_type is User or expected_type.__name__ == "User":
|
254
|
+
return resolved_data.users.get(value) if resolved_data.users else None
|
255
|
+
|
256
|
+
if expected_type is Member or expected_type.__name__ == "Member":
|
257
|
+
member_obj = (
|
258
|
+
resolved_data.members.get(value) if resolved_data.members else None
|
259
|
+
)
|
260
|
+
if member_obj:
|
261
|
+
if (
|
262
|
+
hasattr(member_obj, "username")
|
263
|
+
and not member_obj.username
|
264
|
+
and resolved_data.users
|
265
|
+
):
|
266
|
+
user_obj = resolved_data.users.get(value)
|
267
|
+
if user_obj:
|
268
|
+
member_obj.username = user_obj.username
|
269
|
+
member_obj.discriminator = user_obj.discriminator
|
270
|
+
member_obj.avatar = user_obj.avatar
|
271
|
+
member_obj.bot = user_obj.bot
|
272
|
+
member_obj.user = user_obj # type: ignore[attr-defined]
|
273
|
+
return member_obj
|
274
|
+
return None
|
275
|
+
if expected_type is Role or expected_type.__name__ == "Role":
|
276
|
+
return resolved_data.roles.get(value) if resolved_data.roles else None
|
277
|
+
if expected_type is Attachment or expected_type.__name__ == "Attachment":
|
278
|
+
return (
|
279
|
+
resolved_data.attachments.get(value)
|
280
|
+
if resolved_data.attachments
|
281
|
+
else None
|
282
|
+
)
|
283
|
+
if expected_type is Message or expected_type.__name__ == "Message":
|
284
|
+
return (
|
285
|
+
resolved_data.messages.get(value)
|
286
|
+
if resolved_data.messages
|
287
|
+
else None
|
288
|
+
)
|
289
|
+
if "Channel" in expected_type.__name__:
|
290
|
+
return (
|
291
|
+
resolved_data.channels.get(value)
|
292
|
+
if resolved_data.channels
|
293
|
+
else None
|
294
|
+
)
|
295
|
+
|
296
|
+
# For basic types, Discord already sends them correctly (string, int, bool, float)
|
297
|
+
if isinstance(value, expected_type):
|
298
|
+
return value
|
299
|
+
try: # Attempt direct conversion for basic types if Discord sent string for int/float/bool
|
300
|
+
if expected_type is int:
|
301
|
+
return int(value)
|
302
|
+
if expected_type is float:
|
303
|
+
return float(value)
|
304
|
+
if expected_type is bool: # Discord sends true/false
|
305
|
+
if isinstance(value, str):
|
306
|
+
return value.lower() == "true"
|
307
|
+
return bool(value)
|
308
|
+
except (ValueError, TypeError):
|
309
|
+
pass # Conversion failed
|
310
|
+
return value # Return as is if no specific resolution or conversion applied
|
311
|
+
|
312
|
+
async def _resolve_value(
|
313
|
+
self,
|
314
|
+
value: Any,
|
315
|
+
expected_type: Any,
|
316
|
+
resolved_data: Optional["ResolvedData"],
|
317
|
+
guild_id: Optional["Snowflake"],
|
318
|
+
) -> Any:
|
319
|
+
"""Public wrapper around ``_resolve_option_value`` used by tests."""
|
320
|
+
|
321
|
+
return await self._resolve_option_value(
|
322
|
+
value=value,
|
323
|
+
expected_type=expected_type,
|
324
|
+
resolved_data=resolved_data,
|
325
|
+
guild_id=guild_id,
|
326
|
+
)
|
327
|
+
|
328
|
+
async def _parse_interaction_options(
|
329
|
+
self,
|
330
|
+
command_params: Dict[str, inspect.Parameter], # From command.params
|
331
|
+
interaction_options: Optional[List[Dict[str, Any]]],
|
332
|
+
resolved_data: Optional["ResolvedData"],
|
333
|
+
guild_id: Optional["Snowflake"],
|
334
|
+
) -> Tuple[List[Any], Dict[str, Any]]:
|
335
|
+
"""
|
336
|
+
Parses options from an interaction payload and maps them to command function arguments.
|
337
|
+
"""
|
338
|
+
args_list: List[Any] = []
|
339
|
+
kwargs_dict: Dict[str, Any] = {}
|
340
|
+
|
341
|
+
if not interaction_options: # No options provided in interaction
|
342
|
+
# Check if command has required params without defaults
|
343
|
+
for name, param in command_params.items():
|
344
|
+
if param.default == inspect.Parameter.empty:
|
345
|
+
# This should ideally be caught by Discord if option is marked required
|
346
|
+
raise ValueError(f"Missing required option: {name}")
|
347
|
+
return args_list, kwargs_dict
|
348
|
+
|
349
|
+
# Create a dictionary of provided options by name for easier lookup
|
350
|
+
provided_options: Dict[str, Any] = {
|
351
|
+
opt["name"]: opt["value"] for opt in interaction_options if "value" in opt
|
352
|
+
}
|
353
|
+
|
354
|
+
for name, param in command_params.items():
|
355
|
+
if name in provided_options:
|
356
|
+
raw_value = provided_options[name]
|
357
|
+
expected_type = (
|
358
|
+
param.annotation
|
359
|
+
if param.annotation != inspect.Parameter.empty
|
360
|
+
else str
|
361
|
+
)
|
362
|
+
|
363
|
+
# Handle Optional[T]
|
364
|
+
origin_type = get_origin(expected_type)
|
365
|
+
if origin_type is Union:
|
366
|
+
union_args = get_args(expected_type)
|
367
|
+
# Assuming Optional[T] is Union[T, NoneType]
|
368
|
+
non_none_types = [t for t in union_args if t is not type(None)]
|
369
|
+
if len(non_none_types) == 1:
|
370
|
+
expected_type = non_none_types[0]
|
371
|
+
# Else, complex Union, might need more sophisticated handling or default to raw_value/str
|
372
|
+
elif origin_type is Literal:
|
373
|
+
literal_args = get_args(expected_type)
|
374
|
+
if literal_args:
|
375
|
+
expected_type = type(literal_args[0])
|
376
|
+
else:
|
377
|
+
expected_type = str
|
378
|
+
|
379
|
+
resolved_value = await self._resolve_option_value(
|
380
|
+
raw_value, expected_type, resolved_data, guild_id
|
381
|
+
)
|
382
|
+
|
383
|
+
if (
|
384
|
+
param.kind == inspect.Parameter.KEYWORD_ONLY
|
385
|
+
or param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
386
|
+
):
|
387
|
+
kwargs_dict[name] = resolved_value
|
388
|
+
# Note: Slash commands don't map directly to *args. All options are named.
|
389
|
+
# So, we'll primarily use kwargs_dict and then construct args_list based on param order if needed,
|
390
|
+
# but Discord sends named options, so direct kwarg usage is more natural.
|
391
|
+
elif param.default != inspect.Parameter.empty:
|
392
|
+
kwargs_dict[name] = param.default
|
393
|
+
else:
|
394
|
+
# Required parameter not provided by Discord - this implies an issue with command definition
|
395
|
+
# or Discord's validation, as Discord should enforce required options.
|
396
|
+
raise ValueError(
|
397
|
+
f"Required option '{name}' not found in interaction payload."
|
398
|
+
)
|
399
|
+
|
400
|
+
# Populate args_list based on the order in command_params for positional arguments
|
401
|
+
# This assumes that all args that are not keyword-only are passed positionally if present in kwargs_dict
|
402
|
+
for name, param in command_params.items():
|
403
|
+
if param.kind == inspect.Parameter.POSITIONAL_ONLY or (
|
404
|
+
param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
|
405
|
+
and name in kwargs_dict
|
406
|
+
):
|
407
|
+
if name in kwargs_dict: # Ensure it was resolved or had a default
|
408
|
+
args_list.append(kwargs_dict[name])
|
409
|
+
# If it was POSITIONAL_ONLY and not in kwargs_dict, it's an error (already raised)
|
410
|
+
elif param.kind == inspect.Parameter.VAR_POSITIONAL: # *args
|
411
|
+
# Slash commands don't map to *args well. This would be empty.
|
412
|
+
pass
|
413
|
+
|
414
|
+
# Filter kwargs_dict to only include actual KEYWORD_ONLY or POSITIONAL_OR_KEYWORD params
|
415
|
+
# that were not used for args_list (if strict positional/keyword separation is desired).
|
416
|
+
# For slash commands, it's simpler to pass all resolved named options as kwargs.
|
417
|
+
final_kwargs = {
|
418
|
+
k: v
|
419
|
+
for k, v in kwargs_dict.items()
|
420
|
+
if k in command_params
|
421
|
+
and command_params[k].kind != inspect.Parameter.POSITIONAL_ONLY
|
422
|
+
}
|
423
|
+
|
424
|
+
# For simplicity with slash commands, let's assume all resolved options are passed via kwargs
|
425
|
+
# and the command signature is primarily (self, ctx, **options) or (ctx, **options)
|
426
|
+
# or (self, ctx, option1, option2) where names match.
|
427
|
+
# The AppCommand.invoke will handle passing them.
|
428
|
+
# The current args_list and final_kwargs might be redundant if invoke just uses **final_kwargs.
|
429
|
+
# Let's return kwargs_dict directly for now, and AppCommand.invoke can map them.
|
430
|
+
|
431
|
+
return [], kwargs_dict # Return empty args, all in kwargs for now.
|
432
|
+
|
433
|
+
async def dispatch_app_command_error(
|
434
|
+
self, context: "AppCommandContext", error: Exception
|
435
|
+
) -> None:
|
436
|
+
"""Dispatches an app command error to the client if implemented."""
|
437
|
+
if hasattr(self.client, "on_app_command_error"):
|
438
|
+
await self.client.on_app_command_error(context, error)
|
439
|
+
|
440
|
+
async def process_interaction(self, interaction: "Interaction") -> None:
|
441
|
+
"""Processes an incoming interaction."""
|
442
|
+
if interaction.type == InteractionType.MODAL_SUBMIT:
|
443
|
+
callback = getattr(self.client, "on_modal_submit", None)
|
444
|
+
if callback is not None:
|
445
|
+
from typing import Awaitable, Callable, cast
|
446
|
+
|
447
|
+
await cast(Callable[["Interaction"], Awaitable[None]], callback)(
|
448
|
+
interaction
|
449
|
+
)
|
450
|
+
return
|
451
|
+
|
452
|
+
if interaction.type == InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE:
|
453
|
+
callback = getattr(self.client, "on_autocomplete", None)
|
454
|
+
if callback is not None:
|
455
|
+
from typing import Awaitable, Callable, cast
|
456
|
+
|
457
|
+
await cast(Callable[["Interaction"], Awaitable[None]], callback)(
|
458
|
+
interaction
|
459
|
+
)
|
460
|
+
return
|
461
|
+
|
462
|
+
if interaction.type != InteractionType.APPLICATION_COMMAND:
|
463
|
+
return
|
464
|
+
|
465
|
+
if not interaction.data or not interaction.data.name:
|
466
|
+
from .context import AppCommandContext
|
467
|
+
|
468
|
+
ctx = AppCommandContext(
|
469
|
+
bot=self.client, interaction=interaction, command=None
|
470
|
+
)
|
471
|
+
await ctx.send("Command not found.", ephemeral=True)
|
472
|
+
return
|
473
|
+
|
474
|
+
command_name = interaction.data.name
|
475
|
+
command_type = interaction.data.type or ApplicationCommandType.CHAT_INPUT
|
476
|
+
command = self.get_command(
|
477
|
+
command_name,
|
478
|
+
command_type,
|
479
|
+
interaction.data.options if interaction.data else None,
|
480
|
+
)
|
481
|
+
|
482
|
+
if not command:
|
483
|
+
from .context import AppCommandContext
|
484
|
+
|
485
|
+
ctx = AppCommandContext(
|
486
|
+
bot=self.client, interaction=interaction, command=None
|
487
|
+
)
|
488
|
+
await ctx.send(f"Command '{command_name}' not found.", ephemeral=True)
|
489
|
+
return
|
490
|
+
|
491
|
+
# Create context
|
492
|
+
from .context import AppCommandContext # Ensure AppCommandContext is available
|
493
|
+
|
494
|
+
ctx = AppCommandContext(
|
495
|
+
bot=self.client, interaction=interaction, command=command
|
496
|
+
)
|
497
|
+
|
498
|
+
try:
|
499
|
+
# Prepare arguments for the command callback
|
500
|
+
# Skip 'self' and 'ctx' from command.params for parsing interaction options
|
501
|
+
params_to_parse = {
|
502
|
+
name: param
|
503
|
+
for name, param in command.params.items()
|
504
|
+
if name not in ("self", "ctx")
|
505
|
+
}
|
506
|
+
|
507
|
+
if command.type in (
|
508
|
+
ApplicationCommandType.USER,
|
509
|
+
ApplicationCommandType.MESSAGE,
|
510
|
+
):
|
511
|
+
# Context menu commands provide a target_id. Resolve and pass it
|
512
|
+
args = []
|
513
|
+
kwargs = {}
|
514
|
+
if params_to_parse and interaction.data and interaction.data.target_id:
|
515
|
+
first_param = next(iter(params_to_parse.values()))
|
516
|
+
expected = (
|
517
|
+
first_param.annotation
|
518
|
+
if first_param.annotation != inspect.Parameter.empty
|
519
|
+
else str
|
520
|
+
)
|
521
|
+
resolved = await self._resolve_option_value(
|
522
|
+
interaction.data.target_id,
|
523
|
+
expected,
|
524
|
+
interaction.data.resolved,
|
525
|
+
interaction.guild_id,
|
526
|
+
)
|
527
|
+
if first_param.kind in (
|
528
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
529
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
530
|
+
):
|
531
|
+
args.append(resolved)
|
532
|
+
else:
|
533
|
+
kwargs[first_param.name] = resolved
|
534
|
+
|
535
|
+
await command.invoke(ctx, *args, **kwargs)
|
536
|
+
else:
|
537
|
+
parsed_args, parsed_kwargs = await self._parse_interaction_options(
|
538
|
+
command_params=params_to_parse,
|
539
|
+
interaction_options=interaction.data.options,
|
540
|
+
resolved_data=interaction.data.resolved,
|
541
|
+
guild_id=interaction.guild_id,
|
542
|
+
)
|
543
|
+
|
544
|
+
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
545
|
+
|
546
|
+
except Exception as e:
|
547
|
+
print(f"Error invoking app command '{command.name}': {e}")
|
548
|
+
await self.dispatch_app_command_error(ctx, e)
|
549
|
+
# else:
|
550
|
+
# # Default error reply if no handler on client
|
551
|
+
# try:
|
552
|
+
# await ctx.send(f"An error occurred: {e}", ephemeral=True)
|
553
|
+
# except Exception as send_e:
|
554
|
+
# print(f"Failed to send error message for app command: {send_e}")
|
555
|
+
|
556
|
+
async def sync_commands(
|
557
|
+
self, application_id: "Snowflake", guild_id: Optional["Snowflake"] = None
|
558
|
+
) -> None:
|
559
|
+
"""
|
560
|
+
Synchronizes (registers/updates) all application commands with Discord.
|
561
|
+
If guild_id is provided, syncs commands for that guild. Otherwise, syncs global commands.
|
562
|
+
"""
|
563
|
+
commands_to_sync: List[Dict[str, Any]] = []
|
564
|
+
|
565
|
+
# Collect commands based on scope (global or specific guild)
|
566
|
+
# This needs to be more sophisticated to handle guild_ids on commands/groups
|
567
|
+
|
568
|
+
source_commands = (
|
569
|
+
list(self._slash_commands.values())
|
570
|
+
+ list(self._user_commands.values())
|
571
|
+
+ list(self._message_commands.values())
|
572
|
+
+ list(self._app_command_groups.values())
|
573
|
+
)
|
574
|
+
|
575
|
+
for cmd_or_group in source_commands:
|
576
|
+
# Determine if this command/group should be synced for the current scope
|
577
|
+
is_guild_specific_command = (
|
578
|
+
cmd_or_group.guild_ids is not None and len(cmd_or_group.guild_ids) > 0
|
579
|
+
)
|
580
|
+
|
581
|
+
if guild_id: # Syncing for a specific guild
|
582
|
+
# Skip if not a guild-specific command OR if it's for a different guild
|
583
|
+
if not is_guild_specific_command or (
|
584
|
+
cmd_or_group.guild_ids is not None
|
585
|
+
and guild_id not in cmd_or_group.guild_ids
|
586
|
+
):
|
587
|
+
continue
|
588
|
+
else: # Syncing global commands
|
589
|
+
if is_guild_specific_command:
|
590
|
+
continue # Skip guild-specific commands when syncing global
|
591
|
+
|
592
|
+
# Use the to_dict() method from AppCommand or AppCommandGroup
|
593
|
+
try:
|
594
|
+
payload = cmd_or_group.to_dict()
|
595
|
+
commands_to_sync.append(payload)
|
596
|
+
except AttributeError:
|
597
|
+
print(
|
598
|
+
f"Warning: Command or group '{cmd_or_group.name}' does not have a to_dict() method. Skipping."
|
599
|
+
)
|
600
|
+
except Exception as e:
|
601
|
+
print(
|
602
|
+
f"Error converting command/group '{cmd_or_group.name}' to dict: {e}. Skipping."
|
603
|
+
)
|
604
|
+
|
605
|
+
if not commands_to_sync:
|
606
|
+
print(
|
607
|
+
f"No commands to sync for {'guild ' + str(guild_id) if guild_id else 'global'} scope."
|
608
|
+
)
|
609
|
+
return
|
610
|
+
|
611
|
+
try:
|
612
|
+
if guild_id:
|
613
|
+
print(
|
614
|
+
f"Syncing {len(commands_to_sync)} commands for guild {guild_id}..."
|
615
|
+
)
|
616
|
+
await self.client._http.bulk_overwrite_guild_application_commands(
|
617
|
+
application_id, guild_id, commands_to_sync
|
618
|
+
)
|
619
|
+
else:
|
620
|
+
print(f"Syncing {len(commands_to_sync)} global commands...")
|
621
|
+
await self.client._http.bulk_overwrite_global_application_commands(
|
622
|
+
application_id, commands_to_sync
|
623
|
+
)
|
624
|
+
print("Command sync successful.")
|
625
|
+
except Exception as e:
|
626
|
+
print(f"Error syncing application commands: {e}")
|
627
|
+
# Consider re-raising or specific error handling
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# disagreement/ext/commands/__init__.py
|
2
|
+
|
3
|
+
"""
|
4
|
+
disagreement.ext.commands - A command framework extension for the Disagreement library.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .cog import Cog
|
8
|
+
from .core import (
|
9
|
+
Command,
|
10
|
+
CommandContext,
|
11
|
+
CommandHandler,
|
12
|
+
) # CommandHandler might be internal
|
13
|
+
from .decorators import command, listener, check, check_any, cooldown
|
14
|
+
from .errors import (
|
15
|
+
CommandError,
|
16
|
+
CommandNotFound,
|
17
|
+
BadArgument,
|
18
|
+
MissingRequiredArgument,
|
19
|
+
ArgumentParsingError,
|
20
|
+
CheckFailure,
|
21
|
+
CheckAnyFailure,
|
22
|
+
CommandOnCooldown,
|
23
|
+
CommandInvokeError,
|
24
|
+
)
|
25
|
+
|
26
|
+
__all__ = [
|
27
|
+
# Cog
|
28
|
+
"Cog",
|
29
|
+
# Core
|
30
|
+
"Command",
|
31
|
+
"CommandContext",
|
32
|
+
# "CommandHandler", # Usually not part of public API for direct use by bot devs
|
33
|
+
# Decorators
|
34
|
+
"command",
|
35
|
+
"listener",
|
36
|
+
"check",
|
37
|
+
"check_any",
|
38
|
+
"cooldown",
|
39
|
+
# Errors
|
40
|
+
"CommandError",
|
41
|
+
"CommandNotFound",
|
42
|
+
"BadArgument",
|
43
|
+
"MissingRequiredArgument",
|
44
|
+
"ArgumentParsingError",
|
45
|
+
"CheckFailure",
|
46
|
+
"CheckAnyFailure",
|
47
|
+
"CommandOnCooldown",
|
48
|
+
"CommandInvokeError",
|
49
|
+
]
|