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,569 @@
|
|
1
|
+
# disagreement/ext/app_commands/decorators.py
|
2
|
+
|
3
|
+
import inspect
|
4
|
+
import asyncio
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import (
|
7
|
+
Callable,
|
8
|
+
Optional,
|
9
|
+
List,
|
10
|
+
Dict,
|
11
|
+
Any,
|
12
|
+
Union,
|
13
|
+
Type,
|
14
|
+
get_origin,
|
15
|
+
get_args,
|
16
|
+
TYPE_CHECKING,
|
17
|
+
Literal,
|
18
|
+
Annotated,
|
19
|
+
TypeVar,
|
20
|
+
cast,
|
21
|
+
)
|
22
|
+
|
23
|
+
from .commands import (
|
24
|
+
SlashCommand,
|
25
|
+
UserCommand,
|
26
|
+
MessageCommand,
|
27
|
+
AppCommand,
|
28
|
+
AppCommandGroup,
|
29
|
+
HybridCommand,
|
30
|
+
)
|
31
|
+
from disagreement.interactions import (
|
32
|
+
ApplicationCommandOption,
|
33
|
+
ApplicationCommandOptionChoice,
|
34
|
+
Snowflake,
|
35
|
+
)
|
36
|
+
from disagreement.enums import (
|
37
|
+
ApplicationCommandOptionType,
|
38
|
+
IntegrationType,
|
39
|
+
InteractionContextType,
|
40
|
+
# Assuming ChannelType will be added to disagreement.enums
|
41
|
+
)
|
42
|
+
|
43
|
+
if TYPE_CHECKING:
|
44
|
+
from disagreement.client import Client # For potential future use
|
45
|
+
from disagreement.models import Channel, User
|
46
|
+
|
47
|
+
# Assuming TextChannel, VoiceChannel etc. might be defined or aliased
|
48
|
+
# For now, we'll use string comparisons for channel types or rely on a yet-to-be-defined ChannelType enum
|
49
|
+
Channel = Any
|
50
|
+
Member = Any
|
51
|
+
Role = Any
|
52
|
+
Attachment = Any
|
53
|
+
# from .cog import Cog # Placeholder
|
54
|
+
else:
|
55
|
+
# Runtime fallbacks for optional model classes
|
56
|
+
from disagreement.models import Channel
|
57
|
+
|
58
|
+
Client = Any # type: ignore
|
59
|
+
User = Any # type: ignore
|
60
|
+
Member = Any # type: ignore
|
61
|
+
Role = Any # type: ignore
|
62
|
+
Attachment = Any # type: ignore
|
63
|
+
|
64
|
+
# Mapping Python types to Discord ApplicationCommandOptionType
|
65
|
+
# This will need to be expanded and made more robust.
|
66
|
+
# Consider using a registry or a more sophisticated type mapping system.
|
67
|
+
_type_mapping: Dict[Any, ApplicationCommandOptionType] = (
|
68
|
+
{ # Changed Type to Any for key due to placeholders
|
69
|
+
str: ApplicationCommandOptionType.STRING,
|
70
|
+
int: ApplicationCommandOptionType.INTEGER,
|
71
|
+
bool: ApplicationCommandOptionType.BOOLEAN,
|
72
|
+
float: ApplicationCommandOptionType.NUMBER, # Discord 'NUMBER' type is for float/double
|
73
|
+
User: ApplicationCommandOptionType.USER,
|
74
|
+
Channel: ApplicationCommandOptionType.CHANNEL,
|
75
|
+
# Placeholders for actual model types from disagreement.models
|
76
|
+
# These will be resolved to their actual types when TYPE_CHECKING is False or via isinstance checks
|
77
|
+
}
|
78
|
+
)
|
79
|
+
|
80
|
+
|
81
|
+
# Helper dataclass for storing extra option metadata
|
82
|
+
@dataclass
|
83
|
+
class OptionMetadata:
|
84
|
+
channel_types: Optional[List[int]] = None
|
85
|
+
min_value: Optional[Union[int, float]] = None
|
86
|
+
max_value: Optional[Union[int, float]] = None
|
87
|
+
min_length: Optional[int] = None
|
88
|
+
max_length: Optional[int] = None
|
89
|
+
autocomplete: bool = False
|
90
|
+
|
91
|
+
|
92
|
+
# Ensure these are updated if model names/locations change
|
93
|
+
if TYPE_CHECKING:
|
94
|
+
# _type_mapping[User] = ApplicationCommandOptionType.USER # Already added above
|
95
|
+
_type_mapping[Member] = ApplicationCommandOptionType.USER # Member implies User
|
96
|
+
_type_mapping[Role] = ApplicationCommandOptionType.ROLE
|
97
|
+
_type_mapping[Attachment] = ApplicationCommandOptionType.ATTACHMENT
|
98
|
+
_type_mapping[Channel] = ApplicationCommandOptionType.CHANNEL
|
99
|
+
|
100
|
+
# TypeVar for the app command decorator factory
|
101
|
+
AppCmdType = TypeVar("AppCmdType", bound=AppCommand)
|
102
|
+
|
103
|
+
|
104
|
+
def _extract_options_from_signature(
|
105
|
+
func: Callable[..., Any], option_meta: Optional[Dict[str, OptionMetadata]] = None
|
106
|
+
) -> List[ApplicationCommandOption]:
|
107
|
+
"""
|
108
|
+
Inspects a function signature and generates ApplicationCommandOption list.
|
109
|
+
"""
|
110
|
+
options: List[ApplicationCommandOption] = []
|
111
|
+
params = inspect.signature(func).parameters
|
112
|
+
|
113
|
+
doc = inspect.getdoc(func)
|
114
|
+
param_descriptions: Dict[str, str] = {}
|
115
|
+
if doc:
|
116
|
+
for line in inspect.cleandoc(doc).splitlines():
|
117
|
+
line = line.strip()
|
118
|
+
if line.startswith(":param"):
|
119
|
+
try:
|
120
|
+
_, rest = line.split(" ", 1)
|
121
|
+
name, desc = rest.split(":", 1)
|
122
|
+
param_descriptions[name.strip()] = desc.strip()
|
123
|
+
except ValueError:
|
124
|
+
continue
|
125
|
+
|
126
|
+
# Skip 'self' (for cogs) and 'ctx' (context) parameters
|
127
|
+
param_iter = iter(params.values())
|
128
|
+
first_param = next(param_iter, None)
|
129
|
+
|
130
|
+
# Heuristic: if the function is bound to a class (cog), 'self' might be the first param.
|
131
|
+
# A more robust way would be to check if `func` is a method of a Cog instance later.
|
132
|
+
# For now, simple name check.
|
133
|
+
if first_param and first_param.name == "self":
|
134
|
+
first_param = next(param_iter, None) # Consume 'self', get next
|
135
|
+
|
136
|
+
if first_param and first_param.name == "ctx": # Consume 'ctx'
|
137
|
+
pass # ctx is handled, now iterate over actual command options
|
138
|
+
elif (
|
139
|
+
first_param
|
140
|
+
): # If first_param was not 'self' and not 'ctx', it's a command option
|
141
|
+
param_iter = iter(params.values()) # Reset iterator to include the first param
|
142
|
+
|
143
|
+
for param in param_iter:
|
144
|
+
if param.name == "self" or param.name == "ctx": # Should have been skipped
|
145
|
+
continue
|
146
|
+
|
147
|
+
if param.kind == param.VAR_POSITIONAL or param.kind == param.VAR_KEYWORD:
|
148
|
+
# *args and **kwargs are not directly supported by slash command options structure.
|
149
|
+
# Could raise an error or ignore. For now, ignore.
|
150
|
+
# print(f"Warning: *args/**kwargs ({param.name}) are not supported for slash command options.")
|
151
|
+
continue
|
152
|
+
|
153
|
+
option_name = param.name
|
154
|
+
option_description = param_descriptions.get(
|
155
|
+
option_name, f"Description for {option_name}"
|
156
|
+
)
|
157
|
+
meta = option_meta.get(option_name) if option_meta else None
|
158
|
+
|
159
|
+
param_type_hint = param.annotation
|
160
|
+
if param_type_hint == inspect.Parameter.empty:
|
161
|
+
# Default to string if no type hint, or raise error.
|
162
|
+
# Forcing type hints is generally better for slash commands.
|
163
|
+
# raise TypeError(f"Option '{option_name}' must have a type hint for slash commands.")
|
164
|
+
param_type_hint = str # Defaulting to string, can be made stricter
|
165
|
+
|
166
|
+
option_type: Optional[ApplicationCommandOptionType] = None
|
167
|
+
choices: Optional[List[ApplicationCommandOptionChoice]] = None
|
168
|
+
|
169
|
+
origin = get_origin(param_type_hint)
|
170
|
+
args = get_args(param_type_hint)
|
171
|
+
|
172
|
+
if origin is Annotated:
|
173
|
+
param_type_hint = args[0]
|
174
|
+
for extra in args[1:]:
|
175
|
+
if isinstance(extra, OptionMetadata):
|
176
|
+
meta = extra
|
177
|
+
origin = get_origin(param_type_hint)
|
178
|
+
args = get_args(param_type_hint)
|
179
|
+
|
180
|
+
actual_type_for_mapping = param_type_hint
|
181
|
+
is_optional = False
|
182
|
+
|
183
|
+
if origin is Union: # Handles Optional[T] which is Union[T, NoneType]
|
184
|
+
# Filter out NoneType to get the actual type for mapping
|
185
|
+
union_types = [t for t in args if t is not type(None)]
|
186
|
+
if len(union_types) == 1:
|
187
|
+
actual_type_for_mapping = union_types[0]
|
188
|
+
is_optional = True
|
189
|
+
else:
|
190
|
+
# More complex Unions are not directly supported by a single option type.
|
191
|
+
# Could default to STRING or raise.
|
192
|
+
# For now, let's assume simple Optional[T] or direct types.
|
193
|
+
# print(f"Warning: Complex Union type for '{option_name}' not fully supported, defaulting to STRING.")
|
194
|
+
actual_type_for_mapping = str
|
195
|
+
|
196
|
+
elif origin is list and len(args) == 1:
|
197
|
+
# List[T] is not a direct option type. Discord handles multiple values for some types
|
198
|
+
# via repeated options or specific component interactions, not directly in slash command options.
|
199
|
+
# This might indicate a need for a different interaction pattern or custom parsing.
|
200
|
+
# For now, treat List[str] as a string, others might error or default.
|
201
|
+
# print(f"Warning: List type for '{option_name}' not directly supported as a single option. Consider type {args[0]}.")
|
202
|
+
actual_type_for_mapping = args[
|
203
|
+
0
|
204
|
+
] # Use the inner type for mapping, but this is a simplification.
|
205
|
+
|
206
|
+
if origin is Literal: # typing.Literal['a', 'b']
|
207
|
+
choices = []
|
208
|
+
for choice_val in args:
|
209
|
+
if not isinstance(choice_val, (str, int, float)):
|
210
|
+
raise TypeError(
|
211
|
+
f"Literal choices for '{option_name}' must be str, int, or float. Got {type(choice_val)}."
|
212
|
+
)
|
213
|
+
choices.append(
|
214
|
+
ApplicationCommandOptionChoice(
|
215
|
+
data={"name": str(choice_val), "value": choice_val}
|
216
|
+
)
|
217
|
+
)
|
218
|
+
# The type of the Literal's arguments determines the option type
|
219
|
+
if choices:
|
220
|
+
literal_arg_type = type(choices[0].value)
|
221
|
+
option_type = _type_mapping.get(literal_arg_type)
|
222
|
+
if (
|
223
|
+
not option_type and literal_arg_type is float
|
224
|
+
): # float maps to NUMBER
|
225
|
+
option_type = ApplicationCommandOptionType.NUMBER
|
226
|
+
|
227
|
+
if not option_type: # If not determined by Literal
|
228
|
+
option_type = _type_mapping.get(actual_type_for_mapping)
|
229
|
+
# Special handling for User, Member, Role, Attachment, Channel if not directly in _type_mapping
|
230
|
+
# This is a bit crude; a proper registry or isinstance checks would be better.
|
231
|
+
if not option_type:
|
232
|
+
if (
|
233
|
+
actual_type_for_mapping.__name__ == "User"
|
234
|
+
or actual_type_for_mapping.__name__ == "Member"
|
235
|
+
):
|
236
|
+
option_type = ApplicationCommandOptionType.USER
|
237
|
+
elif actual_type_for_mapping.__name__ == "Role":
|
238
|
+
option_type = ApplicationCommandOptionType.ROLE
|
239
|
+
elif actual_type_for_mapping.__name__ == "Attachment":
|
240
|
+
option_type = ApplicationCommandOptionType.ATTACHMENT
|
241
|
+
elif (
|
242
|
+
inspect.isclass(actual_type_for_mapping)
|
243
|
+
and isinstance(Channel, type)
|
244
|
+
and issubclass(actual_type_for_mapping, cast(type, Channel))
|
245
|
+
):
|
246
|
+
option_type = ApplicationCommandOptionType.CHANNEL
|
247
|
+
|
248
|
+
if not option_type:
|
249
|
+
# Fallback or error if type couldn't be mapped
|
250
|
+
# print(f"Warning: Could not map type '{actual_type_for_mapping}' for option '{option_name}'. Defaulting to STRING.")
|
251
|
+
option_type = ApplicationCommandOptionType.STRING # Default fallback
|
252
|
+
|
253
|
+
required = (param.default == inspect.Parameter.empty) and not is_optional
|
254
|
+
|
255
|
+
data: Dict[str, Any] = {
|
256
|
+
"name": option_name,
|
257
|
+
"description": option_description,
|
258
|
+
"type": option_type.value,
|
259
|
+
"required": required,
|
260
|
+
"choices": ([c.to_dict() for c in choices] if choices else None),
|
261
|
+
}
|
262
|
+
|
263
|
+
if meta:
|
264
|
+
if meta.channel_types is not None:
|
265
|
+
data["channel_types"] = meta.channel_types
|
266
|
+
if meta.min_value is not None:
|
267
|
+
data["min_value"] = meta.min_value
|
268
|
+
if meta.max_value is not None:
|
269
|
+
data["max_value"] = meta.max_value
|
270
|
+
if meta.min_length is not None:
|
271
|
+
data["min_length"] = meta.min_length
|
272
|
+
if meta.max_length is not None:
|
273
|
+
data["max_length"] = meta.max_length
|
274
|
+
if meta.autocomplete:
|
275
|
+
data["autocomplete"] = True
|
276
|
+
|
277
|
+
options.append(ApplicationCommandOption(data=data))
|
278
|
+
return options
|
279
|
+
|
280
|
+
|
281
|
+
def _app_command_decorator(
|
282
|
+
cls: Type[AppCmdType],
|
283
|
+
option_meta: Optional[Dict[str, OptionMetadata]] = None,
|
284
|
+
**attrs: Any,
|
285
|
+
) -> Callable[[Callable[..., Any]], AppCmdType]:
|
286
|
+
"""Generic factory for creating app command decorators."""
|
287
|
+
|
288
|
+
def decorator(func: Callable[..., Any]) -> AppCmdType:
|
289
|
+
if not asyncio.iscoroutinefunction(func):
|
290
|
+
raise TypeError(
|
291
|
+
"Application command callback must be a coroutine function."
|
292
|
+
)
|
293
|
+
|
294
|
+
name = attrs.pop("name", None) or func.__name__
|
295
|
+
description = attrs.pop("description", None) or inspect.getdoc(func)
|
296
|
+
if description: # Clean up docstring
|
297
|
+
description = inspect.cleandoc(description).split("\n\n", 1)[
|
298
|
+
0
|
299
|
+
] # Use first paragraph
|
300
|
+
|
301
|
+
parent_group = attrs.pop("parent", None)
|
302
|
+
if parent_group and not isinstance(parent_group, AppCommandGroup):
|
303
|
+
raise TypeError(
|
304
|
+
"The 'parent' argument must be an AppCommandGroup instance."
|
305
|
+
)
|
306
|
+
|
307
|
+
# For User/Message commands, description should be empty for payload, but can be stored for help.
|
308
|
+
if cls is UserCommand or cls is MessageCommand:
|
309
|
+
actual_description_for_payload = ""
|
310
|
+
else:
|
311
|
+
actual_description_for_payload = description
|
312
|
+
if not actual_description_for_payload and cls is SlashCommand:
|
313
|
+
raise ValueError(f"Slash command '{name}' must have a description.")
|
314
|
+
|
315
|
+
# Create the command instance
|
316
|
+
cmd_instance = cls(
|
317
|
+
callback=func,
|
318
|
+
name=name,
|
319
|
+
description=actual_description_for_payload, # Use payload-appropriate description
|
320
|
+
**attrs, # Remaining attributes like guild_ids, nsfw, etc.
|
321
|
+
)
|
322
|
+
|
323
|
+
# Store original description if different (e.g. for User/Message commands for help text)
|
324
|
+
if description != actual_description_for_payload:
|
325
|
+
cmd_instance._full_description = (
|
326
|
+
description # Custom attribute for library use
|
327
|
+
)
|
328
|
+
|
329
|
+
if isinstance(cmd_instance, SlashCommand):
|
330
|
+
cmd_instance.options = _extract_options_from_signature(func, option_meta)
|
331
|
+
|
332
|
+
if parent_group:
|
333
|
+
parent_group.add_command(cmd_instance) # This also sets cmd_instance.parent
|
334
|
+
|
335
|
+
# Attach command object to the function for later collection by Cog or Client
|
336
|
+
# This is a common pattern.
|
337
|
+
if hasattr(func, "__app_command_object__"):
|
338
|
+
# Function might already be decorated (e.g. hybrid or stacked decorators)
|
339
|
+
# Decide on behavior: error, overwrite, or store list of commands.
|
340
|
+
# For now, let's assume one app command decorator of a specific type per function.
|
341
|
+
# Hybrid commands will need special handling.
|
342
|
+
print(
|
343
|
+
f"Warning: Function {func.__name__} is already an app command or has one attached. Overwriting."
|
344
|
+
)
|
345
|
+
|
346
|
+
setattr(func, "__app_command_object__", cmd_instance)
|
347
|
+
setattr(cmd_instance, "__app_command_object__", cmd_instance)
|
348
|
+
|
349
|
+
# If the command is a HybridCommand, also set the attribute
|
350
|
+
# that the prefix command system's Cog._inject looks for.
|
351
|
+
if isinstance(cmd_instance, HybridCommand):
|
352
|
+
setattr(func, "__command_object__", cmd_instance)
|
353
|
+
setattr(cmd_instance, "__command_object__", cmd_instance)
|
354
|
+
|
355
|
+
return cmd_instance # Return the command instance itself, not the function
|
356
|
+
# This allows it to be added to cogs/handlers directly.
|
357
|
+
|
358
|
+
return decorator
|
359
|
+
|
360
|
+
|
361
|
+
def slash_command(
|
362
|
+
name: Optional[str] = None,
|
363
|
+
description: Optional[str] = None,
|
364
|
+
guild_ids: Optional[List[Snowflake]] = None,
|
365
|
+
default_member_permissions: Optional[str] = None,
|
366
|
+
nsfw: bool = False,
|
367
|
+
name_localizations: Optional[Dict[str, str]] = None,
|
368
|
+
description_localizations: Optional[Dict[str, str]] = None,
|
369
|
+
integration_types: Optional[List[IntegrationType]] = None,
|
370
|
+
contexts: Optional[List[InteractionContextType]] = None,
|
371
|
+
*,
|
372
|
+
guilds: bool = True,
|
373
|
+
dms: bool = True,
|
374
|
+
private_channels: bool = True,
|
375
|
+
parent: Optional[AppCommandGroup] = None, # Added parent parameter
|
376
|
+
locale: Optional[str] = None,
|
377
|
+
option_meta: Optional[Dict[str, OptionMetadata]] = None,
|
378
|
+
) -> Callable[[Callable[..., Any]], SlashCommand]:
|
379
|
+
"""
|
380
|
+
Decorator to create a CHAT_INPUT (slash) command.
|
381
|
+
Options are inferred from the function's type hints.
|
382
|
+
"""
|
383
|
+
if contexts is None:
|
384
|
+
ctxs: List[InteractionContextType] = []
|
385
|
+
if guilds:
|
386
|
+
ctxs.append(InteractionContextType.GUILD)
|
387
|
+
if dms:
|
388
|
+
ctxs.append(InteractionContextType.BOT_DM)
|
389
|
+
if private_channels:
|
390
|
+
ctxs.append(InteractionContextType.PRIVATE_CHANNEL)
|
391
|
+
if len(ctxs) != 3:
|
392
|
+
contexts = ctxs
|
393
|
+
attrs = {
|
394
|
+
"name": name,
|
395
|
+
"description": description,
|
396
|
+
"guild_ids": guild_ids,
|
397
|
+
"default_member_permissions": default_member_permissions,
|
398
|
+
"nsfw": nsfw,
|
399
|
+
"name_localizations": name_localizations,
|
400
|
+
"description_localizations": description_localizations,
|
401
|
+
"integration_types": integration_types,
|
402
|
+
"contexts": contexts,
|
403
|
+
"parent": parent, # Pass parent to attrs
|
404
|
+
"locale": locale,
|
405
|
+
}
|
406
|
+
# Filter out None values to avoid passing them as explicit None to command constructor
|
407
|
+
# Keep 'parent' even if None, as _app_command_decorator handles None parent.
|
408
|
+
# nsfw default is False, so it's fine if not present and defaults.
|
409
|
+
attrs = {k: v for k, v in attrs.items() if v is not None or k in ["nsfw", "parent"]}
|
410
|
+
return _app_command_decorator(SlashCommand, option_meta, **attrs)
|
411
|
+
|
412
|
+
|
413
|
+
def user_command(
|
414
|
+
name: Optional[str] = None,
|
415
|
+
guild_ids: Optional[List[Snowflake]] = None,
|
416
|
+
default_member_permissions: Optional[str] = None,
|
417
|
+
nsfw: bool = False, # Though less common for user commands
|
418
|
+
name_localizations: Optional[Dict[str, str]] = None,
|
419
|
+
integration_types: Optional[List[IntegrationType]] = None,
|
420
|
+
contexts: Optional[List[InteractionContextType]] = None,
|
421
|
+
locale: Optional[str] = None,
|
422
|
+
# description is not used by Discord for User commands
|
423
|
+
) -> Callable[[Callable[..., Any]], UserCommand]:
|
424
|
+
"""Decorator to create a USER context menu command."""
|
425
|
+
attrs = {
|
426
|
+
"name": name,
|
427
|
+
"guild_ids": guild_ids,
|
428
|
+
"default_member_permissions": default_member_permissions,
|
429
|
+
"nsfw": nsfw,
|
430
|
+
"name_localizations": name_localizations,
|
431
|
+
"integration_types": integration_types,
|
432
|
+
"contexts": contexts,
|
433
|
+
"locale": locale,
|
434
|
+
}
|
435
|
+
attrs = {k: v for k, v in attrs.items() if v is not None or k in ["nsfw"]}
|
436
|
+
return _app_command_decorator(UserCommand, **attrs)
|
437
|
+
|
438
|
+
|
439
|
+
def message_command(
|
440
|
+
name: Optional[str] = None,
|
441
|
+
guild_ids: Optional[List[Snowflake]] = None,
|
442
|
+
default_member_permissions: Optional[str] = None,
|
443
|
+
nsfw: bool = False, # Though less common for message commands
|
444
|
+
name_localizations: Optional[Dict[str, str]] = None,
|
445
|
+
integration_types: Optional[List[IntegrationType]] = None,
|
446
|
+
contexts: Optional[List[InteractionContextType]] = None,
|
447
|
+
locale: Optional[str] = None,
|
448
|
+
# description is not used by Discord for Message commands
|
449
|
+
) -> Callable[[Callable[..., Any]], MessageCommand]:
|
450
|
+
"""Decorator to create a MESSAGE context menu command."""
|
451
|
+
attrs = {
|
452
|
+
"name": name,
|
453
|
+
"guild_ids": guild_ids,
|
454
|
+
"default_member_permissions": default_member_permissions,
|
455
|
+
"nsfw": nsfw,
|
456
|
+
"name_localizations": name_localizations,
|
457
|
+
"integration_types": integration_types,
|
458
|
+
"contexts": contexts,
|
459
|
+
"locale": locale,
|
460
|
+
}
|
461
|
+
attrs = {k: v for k, v in attrs.items() if v is not None or k in ["nsfw"]}
|
462
|
+
return _app_command_decorator(MessageCommand, **attrs)
|
463
|
+
|
464
|
+
|
465
|
+
def hybrid_command(
|
466
|
+
name: Optional[str] = None,
|
467
|
+
description: Optional[str] = None,
|
468
|
+
guild_ids: Optional[List[Snowflake]] = None,
|
469
|
+
default_member_permissions: Optional[str] = None,
|
470
|
+
nsfw: bool = False,
|
471
|
+
name_localizations: Optional[Dict[str, str]] = None,
|
472
|
+
description_localizations: Optional[Dict[str, str]] = None,
|
473
|
+
integration_types: Optional[List[IntegrationType]] = None,
|
474
|
+
contexts: Optional[List[InteractionContextType]] = None,
|
475
|
+
*,
|
476
|
+
guilds: bool = True,
|
477
|
+
dms: bool = True,
|
478
|
+
private_channels: bool = True,
|
479
|
+
aliases: Optional[List[str]] = None, # Specific to prefix command aspect
|
480
|
+
# Other prefix-specific options can be added here (e.g., help, brief)
|
481
|
+
option_meta: Optional[Dict[str, OptionMetadata]] = None,
|
482
|
+
locale: Optional[str] = None,
|
483
|
+
) -> Callable[[Callable[..., Any]], HybridCommand]:
|
484
|
+
"""
|
485
|
+
Decorator to create a command that can be invoked as both a slash command
|
486
|
+
and a traditional prefix-based command.
|
487
|
+
Options for the slash command part are inferred from the function's type hints.
|
488
|
+
"""
|
489
|
+
if contexts is None:
|
490
|
+
ctxs: List[InteractionContextType] = []
|
491
|
+
if guilds:
|
492
|
+
ctxs.append(InteractionContextType.GUILD)
|
493
|
+
if dms:
|
494
|
+
ctxs.append(InteractionContextType.BOT_DM)
|
495
|
+
if private_channels:
|
496
|
+
ctxs.append(InteractionContextType.PRIVATE_CHANNEL)
|
497
|
+
if len(ctxs) != 3:
|
498
|
+
contexts = ctxs
|
499
|
+
attrs = {
|
500
|
+
"name": name,
|
501
|
+
"description": description,
|
502
|
+
"guild_ids": guild_ids,
|
503
|
+
"default_member_permissions": default_member_permissions,
|
504
|
+
"nsfw": nsfw,
|
505
|
+
"name_localizations": name_localizations,
|
506
|
+
"description_localizations": description_localizations,
|
507
|
+
"integration_types": integration_types,
|
508
|
+
"contexts": contexts,
|
509
|
+
"aliases": aliases or [], # Ensure aliases is a list
|
510
|
+
"locale": locale,
|
511
|
+
}
|
512
|
+
# Filter out None values to avoid passing them as explicit None to command constructor
|
513
|
+
# Keep 'nsfw' and 'aliases' as they have defaults (False, [])
|
514
|
+
attrs = {
|
515
|
+
k: v for k, v in attrs.items() if v is not None or k in ["nsfw", "aliases"]
|
516
|
+
}
|
517
|
+
return _app_command_decorator(HybridCommand, option_meta, **attrs)
|
518
|
+
|
519
|
+
|
520
|
+
def subcommand(
|
521
|
+
parent: AppCommandGroup, *d_args: Any, **d_kwargs: Any
|
522
|
+
) -> Callable[[Callable[..., Any]], SlashCommand]:
|
523
|
+
"""Create a subcommand under an existing :class:`AppCommandGroup`."""
|
524
|
+
|
525
|
+
d_kwargs.setdefault("parent", parent)
|
526
|
+
return slash_command(*d_args, **d_kwargs)
|
527
|
+
|
528
|
+
|
529
|
+
def group(
|
530
|
+
name: str,
|
531
|
+
description: Optional[str] = None,
|
532
|
+
**kwargs: Any,
|
533
|
+
) -> Callable[[Optional[Callable[..., Any]]], AppCommandGroup]:
|
534
|
+
"""Decorator to declare a top level :class:`AppCommandGroup`."""
|
535
|
+
|
536
|
+
def decorator(func: Optional[Callable[..., Any]] = None) -> AppCommandGroup:
|
537
|
+
grp = AppCommandGroup(
|
538
|
+
name=name,
|
539
|
+
description=description,
|
540
|
+
guild_ids=kwargs.get("guild_ids"),
|
541
|
+
parent=kwargs.get("parent"),
|
542
|
+
default_member_permissions=kwargs.get("default_member_permissions"),
|
543
|
+
nsfw=kwargs.get("nsfw", False),
|
544
|
+
name_localizations=kwargs.get("name_localizations"),
|
545
|
+
description_localizations=kwargs.get("description_localizations"),
|
546
|
+
integration_types=kwargs.get("integration_types"),
|
547
|
+
contexts=kwargs.get("contexts"),
|
548
|
+
)
|
549
|
+
|
550
|
+
if func is not None:
|
551
|
+
setattr(func, "__app_command_object__", grp)
|
552
|
+
return grp
|
553
|
+
|
554
|
+
return decorator
|
555
|
+
|
556
|
+
|
557
|
+
def subcommand_group(
|
558
|
+
parent: AppCommandGroup,
|
559
|
+
name: str,
|
560
|
+
description: Optional[str] = None,
|
561
|
+
**kwargs: Any,
|
562
|
+
) -> Callable[[Optional[Callable[..., Any]]], AppCommandGroup]:
|
563
|
+
"""Create a nested :class:`AppCommandGroup` under ``parent``."""
|
564
|
+
|
565
|
+
return parent.group(
|
566
|
+
name=name,
|
567
|
+
description=description,
|
568
|
+
**kwargs,
|
569
|
+
)
|