disagreement 0.0.2__py3-none-any.whl → 0.1.0rc2__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 +8 -3
- disagreement/audio.py +116 -0
- disagreement/client.py +217 -8
- disagreement/color.py +78 -0
- disagreement/components.py +2 -2
- disagreement/enums.py +5 -0
- disagreement/errors.py +13 -8
- disagreement/event_dispatcher.py +102 -45
- disagreement/ext/app_commands/__init__.py +2 -0
- disagreement/ext/app_commands/commands.py +13 -99
- disagreement/ext/app_commands/decorators.py +1 -1
- disagreement/ext/app_commands/hybrid.py +61 -0
- disagreement/ext/commands/__init__.py +9 -1
- disagreement/ext/commands/core.py +15 -2
- disagreement/ext/commands/decorators.py +72 -30
- disagreement/ext/loader.py +12 -1
- disagreement/ext/tasks.py +147 -8
- disagreement/gateway.py +56 -13
- disagreement/http.py +219 -16
- disagreement/interactions.py +17 -14
- disagreement/models.py +432 -7
- disagreement/shard_manager.py +2 -0
- disagreement/ui/modal.py +1 -1
- disagreement/utils.py +73 -0
- disagreement/voice_client.py +42 -0
- {disagreement-0.0.2.dist-info → disagreement-0.1.0rc2.dist-info}/METADATA +14 -6
- disagreement-0.1.0rc2.dist-info/RECORD +53 -0
- disagreement-0.0.2.dist-info/RECORD +0 -49
- {disagreement-0.0.2.dist-info → disagreement-0.1.0rc2.dist-info}/WHEEL +0 -0
- {disagreement-0.0.2.dist-info → disagreement-0.1.0rc2.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.0.2.dist-info → disagreement-0.1.0rc2.dist-info}/top_level.txt +0 -0
disagreement/event_dispatcher.py
CHANGED
@@ -47,10 +47,20 @@ class EventDispatcher:
|
|
47
47
|
# Pre-defined parsers for specific event types to convert raw data to models
|
48
48
|
self._event_parsers: Dict[str, Callable[[Dict[str, Any]], Any]] = {
|
49
49
|
"MESSAGE_CREATE": self._parse_message_create,
|
50
|
+
"MESSAGE_UPDATE": self._parse_message_update,
|
51
|
+
"MESSAGE_DELETE": self._parse_message_delete,
|
52
|
+
"MESSAGE_REACTION_ADD": self._parse_message_reaction,
|
53
|
+
"MESSAGE_REACTION_REMOVE": self._parse_message_reaction,
|
50
54
|
"INTERACTION_CREATE": self._parse_interaction_create,
|
51
55
|
"GUILD_CREATE": self._parse_guild_create,
|
52
56
|
"CHANNEL_CREATE": self._parse_channel_create,
|
57
|
+
"CHANNEL_UPDATE": self._parse_channel_update,
|
53
58
|
"PRESENCE_UPDATE": self._parse_presence_update,
|
59
|
+
"GUILD_MEMBER_ADD": self._parse_guild_member_add,
|
60
|
+
"GUILD_MEMBER_REMOVE": self._parse_guild_member_remove,
|
61
|
+
"GUILD_BAN_ADD": self._parse_guild_ban_add,
|
62
|
+
"GUILD_BAN_REMOVE": self._parse_guild_ban_remove,
|
63
|
+
"GUILD_ROLE_UPDATE": self._parse_guild_role_update,
|
54
64
|
"TYPING_START": self._parse_typing_start,
|
55
65
|
}
|
56
66
|
|
@@ -58,6 +68,21 @@ class EventDispatcher:
|
|
58
68
|
"""Parses raw MESSAGE_CREATE data into a Message object."""
|
59
69
|
return self._client.parse_message(data)
|
60
70
|
|
71
|
+
def _parse_message_update(self, data: Dict[str, Any]) -> Message:
|
72
|
+
"""Parses raw MESSAGE_UPDATE data into a Message object."""
|
73
|
+
return self._client.parse_message(data)
|
74
|
+
|
75
|
+
def _parse_message_delete(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
76
|
+
"""Parses MESSAGE_DELETE and updates message cache."""
|
77
|
+
message_id = data.get("id")
|
78
|
+
if message_id:
|
79
|
+
self._client._messages.pop(message_id, None)
|
80
|
+
return data
|
81
|
+
|
82
|
+
def _parse_message_reaction_raw(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
83
|
+
"""Returns the raw reaction payload."""
|
84
|
+
return data
|
85
|
+
|
61
86
|
def _parse_interaction_create(self, data: Dict[str, Any]) -> "Interaction":
|
62
87
|
"""Parses raw INTERACTION_CREATE data into an Interaction object."""
|
63
88
|
from .interactions import Interaction
|
@@ -88,6 +113,52 @@ class EventDispatcher:
|
|
88
113
|
|
89
114
|
return TypingStart(data, client_instance=self._client)
|
90
115
|
|
116
|
+
def _parse_message_reaction(self, data: Dict[str, Any]):
|
117
|
+
"""Parses raw reaction data into a Reaction object."""
|
118
|
+
|
119
|
+
from .models import Reaction
|
120
|
+
|
121
|
+
return Reaction(data, client_instance=self._client)
|
122
|
+
|
123
|
+
def _parse_guild_member_add(self, data: Dict[str, Any]):
|
124
|
+
"""Parses GUILD_MEMBER_ADD into a Member object."""
|
125
|
+
|
126
|
+
guild_id = str(data.get("guild_id"))
|
127
|
+
return self._client.parse_member(data, guild_id)
|
128
|
+
|
129
|
+
def _parse_guild_member_remove(self, data: Dict[str, Any]):
|
130
|
+
"""Parses GUILD_MEMBER_REMOVE into a GuildMemberRemove model."""
|
131
|
+
|
132
|
+
from .models import GuildMemberRemove
|
133
|
+
|
134
|
+
return GuildMemberRemove(data, client_instance=self._client)
|
135
|
+
|
136
|
+
def _parse_guild_ban_add(self, data: Dict[str, Any]):
|
137
|
+
"""Parses GUILD_BAN_ADD into a GuildBanAdd model."""
|
138
|
+
|
139
|
+
from .models import GuildBanAdd
|
140
|
+
|
141
|
+
return GuildBanAdd(data, client_instance=self._client)
|
142
|
+
|
143
|
+
def _parse_guild_ban_remove(self, data: Dict[str, Any]):
|
144
|
+
"""Parses GUILD_BAN_REMOVE into a GuildBanRemove model."""
|
145
|
+
|
146
|
+
from .models import GuildBanRemove
|
147
|
+
|
148
|
+
return GuildBanRemove(data, client_instance=self._client)
|
149
|
+
|
150
|
+
def _parse_channel_update(self, data: Dict[str, Any]):
|
151
|
+
"""Parses CHANNEL_UPDATE into a Channel object."""
|
152
|
+
|
153
|
+
return self._client.parse_channel(data)
|
154
|
+
|
155
|
+
def _parse_guild_role_update(self, data: Dict[str, Any]):
|
156
|
+
"""Parses GUILD_ROLE_UPDATE into a GuildRoleUpdate model."""
|
157
|
+
|
158
|
+
from .models import GuildRoleUpdate
|
159
|
+
|
160
|
+
return GuildRoleUpdate(data, client_instance=self._client)
|
161
|
+
|
91
162
|
# Potentially add _parse_user for events that directly provide a full user object
|
92
163
|
# def _parse_user_update(self, data: Dict[str, Any]) -> User:
|
93
164
|
# return User(data=data)
|
@@ -169,75 +240,61 @@ class EventDispatcher:
|
|
169
240
|
if not waiters:
|
170
241
|
self._waiters.pop(event_name, None)
|
171
242
|
|
172
|
-
async def
|
173
|
-
|
174
|
-
Dispatches an event to all registered listeners.
|
175
|
-
|
176
|
-
Args:
|
177
|
-
event_name (str): The name of the event (e.g., 'MESSAGE_CREATE').
|
178
|
-
raw_data (Dict[str, Any]): The raw data payload from the Discord Gateway for this event.
|
179
|
-
"""
|
180
|
-
event_name_upper = event_name.upper()
|
181
|
-
listeners = self._listeners.get(event_name_upper)
|
182
|
-
|
243
|
+
async def _dispatch_to_listeners(self, event_name: str, data: Any) -> None:
|
244
|
+
listeners = self._listeners.get(event_name)
|
183
245
|
if not listeners:
|
184
|
-
# print(f"No listeners for event {event_name_upper}")
|
185
246
|
return
|
186
247
|
|
187
|
-
|
188
|
-
if event_name_upper in self._event_parsers:
|
189
|
-
try:
|
190
|
-
parser = self._event_parsers[event_name_upper]
|
191
|
-
parsed_data = parser(raw_data)
|
192
|
-
except Exception as e:
|
193
|
-
print(f"Error parsing event data for {event_name_upper}: {e}")
|
194
|
-
# Optionally, dispatch with raw_data or raise, or log more formally
|
195
|
-
# For now, we'll proceed to dispatch with raw_data if parsing fails,
|
196
|
-
# or just log and return if parsed_data is critical.
|
197
|
-
# Let's assume if a parser exists, its output is critical.
|
198
|
-
return
|
248
|
+
self._resolve_waiters(event_name, data)
|
199
249
|
|
200
|
-
self._resolve_waiters(event_name_upper, parsed_data)
|
201
|
-
# print(f"Dispatching event {event_name_upper} with data: {parsed_data} to {len(listeners)} listeners.")
|
202
250
|
for listener in listeners:
|
203
251
|
try:
|
204
|
-
# Inspect the listener to see how many arguments it expects
|
205
252
|
sig = inspect.signature(listener)
|
206
253
|
num_params = len(sig.parameters)
|
207
254
|
|
208
|
-
if num_params == 0:
|
255
|
+
if num_params == 0:
|
209
256
|
await listener()
|
210
|
-
elif
|
211
|
-
|
212
|
-
): # Listener takes one argument (the parsed data or model)
|
213
|
-
await listener(parsed_data)
|
214
|
-
# elif num_params == 2 and event_name_upper == "MESSAGE_CREATE": # Special case for (client, message)
|
215
|
-
# await listener(self._client, parsed_data) # This might be too specific here
|
257
|
+
elif num_params == 1:
|
258
|
+
await listener(data)
|
216
259
|
else:
|
217
|
-
# Fallback or error if signature doesn't match expected patterns
|
218
|
-
# For now, assume one arg is the most common for parsed data.
|
219
|
-
# Or, if you want to be strict:
|
220
260
|
print(
|
221
|
-
f"Warning: Listener {listener.__name__} for {
|
261
|
+
f"Warning: Listener {listener.__name__} for {event_name} has an unhandled number of parameters ({num_params}). Skipping or attempting with one arg."
|
222
262
|
)
|
223
|
-
if num_params > 0:
|
224
|
-
await listener(
|
263
|
+
if num_params > 0:
|
264
|
+
await listener(data)
|
225
265
|
|
226
266
|
except Exception as e:
|
227
267
|
callback = self.on_dispatch_error
|
228
268
|
if callback is not None:
|
229
269
|
try:
|
230
|
-
await callback(
|
231
|
-
|
270
|
+
await callback(event_name, e, listener)
|
232
271
|
except Exception as hook_error:
|
233
272
|
print(f"Error in on_dispatch_error hook itself: {hook_error}")
|
234
273
|
else:
|
235
|
-
# Default error handling if no hook is set
|
236
274
|
print(
|
237
|
-
f"Error in event listener {listener.__name__} for {
|
275
|
+
f"Error in event listener {listener.__name__} for {event_name}: {e}"
|
238
276
|
)
|
239
277
|
if hasattr(self._client, "on_error"):
|
240
278
|
try:
|
241
|
-
await self._client.on_error(
|
279
|
+
await self._client.on_error(event_name, e, listener)
|
242
280
|
except Exception as client_err_e:
|
243
281
|
print(f"Error in client.on_error itself: {client_err_e}")
|
282
|
+
|
283
|
+
async def dispatch(self, event_name: str, raw_data: Dict[str, Any]):
|
284
|
+
"""Dispatch an event and its raw counterpart to all listeners."""
|
285
|
+
|
286
|
+
event_name_upper = event_name.upper()
|
287
|
+
raw_event_name = f"RAW_{event_name_upper}"
|
288
|
+
|
289
|
+
await self._dispatch_to_listeners(raw_event_name, raw_data)
|
290
|
+
|
291
|
+
parsed_data: Any = raw_data
|
292
|
+
if event_name_upper in self._event_parsers:
|
293
|
+
try:
|
294
|
+
parser = self._event_parsers[event_name_upper]
|
295
|
+
parsed_data = parser(raw_data)
|
296
|
+
except Exception as e:
|
297
|
+
print(f"Error parsing event data for {event_name_upper}: {e}")
|
298
|
+
return
|
299
|
+
|
300
|
+
await self._dispatch_to_listeners(event_name_upper, parsed_data)
|
@@ -1,58 +1,25 @@
|
|
1
1
|
# disagreement/ext/app_commands/commands.py
|
2
2
|
|
3
3
|
import inspect
|
4
|
-
from typing import Callable,
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Union, TYPE_CHECKING
|
5
5
|
|
6
|
+
from disagreement.enums import (
|
7
|
+
ApplicationCommandType,
|
8
|
+
ApplicationCommandOptionType,
|
9
|
+
IntegrationType,
|
10
|
+
InteractionContextType,
|
11
|
+
)
|
12
|
+
from disagreement.interactions import ApplicationCommandOption, Snowflake
|
6
13
|
|
7
14
|
if TYPE_CHECKING:
|
8
|
-
from disagreement.ext.commands.core import
|
9
|
-
|
10
|
-
|
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
|
15
|
+
from disagreement.ext.commands.core import Command as PrefixCommand
|
16
|
+
from disagreement.ext.commands.cog import Cog
|
17
|
+
|
21
18
|
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
19
|
try:
|
37
20
|
from disagreement.ext.commands.core import Command as PrefixCommand
|
38
|
-
except
|
39
|
-
PrefixCommand = Any
|
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
|
21
|
+
except ImportError:
|
22
|
+
PrefixCommand = Any
|
56
23
|
|
57
24
|
|
58
25
|
class AppCommand:
|
@@ -235,59 +202,6 @@ class MessageCommand(AppCommand):
|
|
235
202
|
super().__init__(callback, type=ApplicationCommandType.MESSAGE, **kwargs)
|
236
203
|
|
237
204
|
|
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
205
|
|
292
206
|
|
293
207
|
class AppCommandGroup:
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# disagreement/ext/app_commands/hybrid.py
|
2
|
+
|
3
|
+
from typing import Any, Callable, List, Optional
|
4
|
+
|
5
|
+
from .commands import SlashCommand
|
6
|
+
from disagreement.ext.commands.core import PrefixCommand
|
7
|
+
|
8
|
+
|
9
|
+
class HybridCommand(SlashCommand, PrefixCommand): # Inherit from both
|
10
|
+
"""
|
11
|
+
Represents a command that can be invoked as both a slash command
|
12
|
+
and a traditional prefix-based command.
|
13
|
+
"""
|
14
|
+
|
15
|
+
def __init__(self, callback: Callable[..., Any], **kwargs: Any):
|
16
|
+
# Initialize SlashCommand part (which calls AppCommand.__init__)
|
17
|
+
# We need to ensure 'type' is correctly passed for AppCommand
|
18
|
+
# kwargs for SlashCommand: name, description, guild_ids, default_member_permissions, nsfw, parent, cog, etc.
|
19
|
+
# kwargs for PrefixCommand: name, aliases, brief, description, cog
|
20
|
+
|
21
|
+
# Pop prefix-specific args before passing to SlashCommand constructor
|
22
|
+
prefix_aliases = kwargs.pop("aliases", [])
|
23
|
+
prefix_brief = kwargs.pop("brief", None)
|
24
|
+
# Description is used by both, AppCommand's constructor will handle it.
|
25
|
+
# Name is used by both. Cog is used by both.
|
26
|
+
|
27
|
+
# Call SlashCommand's __init__
|
28
|
+
# This will set up name, description, callback, type=CHAT_INPUT, options, etc.
|
29
|
+
super().__init__(callback, **kwargs) # This is SlashCommand.__init__
|
30
|
+
|
31
|
+
# Now, explicitly initialize the PrefixCommand parts that SlashCommand didn't cover
|
32
|
+
# or that need specific values for the prefix version.
|
33
|
+
# PrefixCommand.__init__(self, callback, name=self.name, aliases=prefix_aliases, brief=prefix_brief, description=self.description, cog=self.cog)
|
34
|
+
# However, PrefixCommand.__init__ also sets self.params, which AppCommand already did.
|
35
|
+
# We need to be careful not to re-initialize things unnecessarily or incorrectly.
|
36
|
+
# Let's manually set the distinct attributes for the PrefixCommand aspect.
|
37
|
+
|
38
|
+
# Attributes from PrefixCommand:
|
39
|
+
# self.callback is already set by AppCommand
|
40
|
+
# self.name is already set by AppCommand
|
41
|
+
self.aliases: List[str] = (
|
42
|
+
prefix_aliases # This was specific to HybridCommand before, now aligns with PrefixCommand
|
43
|
+
)
|
44
|
+
self.brief: Optional[str] = prefix_brief
|
45
|
+
# self.description is already set by AppCommand (SlashCommand ensures it exists)
|
46
|
+
# self.cog is already set by AppCommand
|
47
|
+
# self.params is already set by AppCommand
|
48
|
+
|
49
|
+
# Ensure the MRO is handled correctly. Python's MRO (C3 linearization)
|
50
|
+
# should call SlashCommand's __init__ then AppCommand's __init__.
|
51
|
+
# PrefixCommand.__init__ won't be called automatically unless we explicitly call it.
|
52
|
+
# By setting attributes directly, we avoid potential issues with multiple __init__ calls
|
53
|
+
# if their logic overlaps too much (e.g., both trying to set self.params).
|
54
|
+
|
55
|
+
# We might need to override invoke if the context or argument passing differs significantly
|
56
|
+
# between app command invocation and prefix command invocation.
|
57
|
+
# For now, SlashCommand.invoke and PrefixCommand.invoke are separate.
|
58
|
+
# The correct one will be called depending on how the command is dispatched.
|
59
|
+
# The AppCommandHandler will use AppCommand.invoke (via SlashCommand).
|
60
|
+
# The prefix CommandHandler will use PrefixCommand.invoke.
|
61
|
+
# This seems acceptable.
|
@@ -10,7 +10,14 @@ from .core import (
|
|
10
10
|
CommandContext,
|
11
11
|
CommandHandler,
|
12
12
|
) # CommandHandler might be internal
|
13
|
-
from .decorators import
|
13
|
+
from .decorators import (
|
14
|
+
command,
|
15
|
+
listener,
|
16
|
+
check,
|
17
|
+
check_any,
|
18
|
+
cooldown,
|
19
|
+
requires_permissions,
|
20
|
+
)
|
14
21
|
from .errors import (
|
15
22
|
CommandError,
|
16
23
|
CommandNotFound,
|
@@ -36,6 +43,7 @@ __all__ = [
|
|
36
43
|
"check",
|
37
44
|
"check_any",
|
38
45
|
"cooldown",
|
46
|
+
"requires_permissions",
|
39
47
|
# Errors
|
40
48
|
"CommandError",
|
41
49
|
"CommandNotFound",
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# disagreement/ext/commands/core.py
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
|
3
5
|
import asyncio
|
4
6
|
import inspect
|
5
7
|
from typing import (
|
@@ -27,10 +29,10 @@ from .errors import (
|
|
27
29
|
CommandInvokeError,
|
28
30
|
)
|
29
31
|
from .converters import run_converters, DEFAULT_CONVERTERS, Converter
|
30
|
-
from .cog import Cog
|
31
32
|
from disagreement.typing import Typing
|
32
33
|
|
33
34
|
if TYPE_CHECKING:
|
35
|
+
from .cog import Cog
|
34
36
|
from disagreement.client import Client
|
35
37
|
from disagreement.models import Message, User
|
36
38
|
|
@@ -86,6 +88,9 @@ class Command:
|
|
86
88
|
await self.callback(ctx, *args, **kwargs)
|
87
89
|
|
88
90
|
|
91
|
+
PrefixCommand = Command # Alias for clarity in hybrid commands
|
92
|
+
|
93
|
+
|
89
94
|
class CommandContext:
|
90
95
|
"""
|
91
96
|
Represents the context in which a command is being invoked.
|
@@ -114,9 +119,16 @@ class CommandContext:
|
|
114
119
|
|
115
120
|
self.author: "User" = message.author
|
116
121
|
|
122
|
+
@property
|
123
|
+
def guild(self):
|
124
|
+
"""The guild this command was invoked in."""
|
125
|
+
if self.message.guild_id and hasattr(self.bot, "get_guild"):
|
126
|
+
return self.bot.get_guild(self.message.guild_id)
|
127
|
+
return None
|
128
|
+
|
117
129
|
async def reply(
|
118
130
|
self,
|
119
|
-
content: str,
|
131
|
+
content: Optional[str] = None,
|
120
132
|
*,
|
121
133
|
mention_author: Optional[bool] = None,
|
122
134
|
**kwargs: Any,
|
@@ -228,6 +240,7 @@ class CommandHandler:
|
|
228
240
|
return self.commands.get(name.lower())
|
229
241
|
|
230
242
|
def add_cog(self, cog_to_add: "Cog") -> None:
|
243
|
+
from .cog import Cog
|
231
244
|
if not isinstance(cog_to_add, Cog):
|
232
245
|
raise TypeError("Argument must be a subclass of Cog.")
|
233
246
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# disagreement/ext/commands/decorators.py
|
2
|
+
from __future__ import annotations
|
2
3
|
|
3
4
|
import asyncio
|
4
5
|
import inspect
|
@@ -6,9 +7,9 @@ import time
|
|
6
7
|
from typing import Callable, Any, Optional, List, TYPE_CHECKING, Awaitable
|
7
8
|
|
8
9
|
if TYPE_CHECKING:
|
9
|
-
from .core import Command, CommandContext
|
10
|
-
|
11
|
-
|
10
|
+
from .core import Command, CommandContext
|
11
|
+
from disagreement.permissions import Permissions
|
12
|
+
from disagreement.models import Member, Guild, Channel
|
12
13
|
|
13
14
|
|
14
15
|
def command(
|
@@ -33,32 +34,16 @@ def command(
|
|
33
34
|
if not asyncio.iscoroutinefunction(func):
|
34
35
|
raise TypeError("Command callback must be a coroutine function.")
|
35
36
|
|
36
|
-
from .core import
|
37
|
-
Command,
|
38
|
-
) # Late import to avoid circular dependencies at module load time
|
39
|
-
|
40
|
-
# The actual registration will happen when a Cog is added or if commands are global.
|
41
|
-
# For now, this decorator creates a Command instance and attaches it to the function,
|
42
|
-
# or returns a Command instance that can be collected.
|
37
|
+
from .core import Command
|
43
38
|
|
44
39
|
cmd_name = name or func.__name__
|
45
40
|
|
46
|
-
# Store command attributes on the function itself for later collection by Cog or Client
|
47
|
-
# This is a common pattern.
|
48
41
|
if hasattr(func, "__command_attrs__"):
|
49
|
-
# This case might occur if decorators are stacked in an unusual way,
|
50
|
-
# or if a function is decorated multiple times (which should be disallowed or handled).
|
51
|
-
# For now, let's assume one @command decorator per function.
|
52
42
|
raise TypeError("Function is already a command or has command attributes.")
|
53
43
|
|
54
|
-
# Create the command object. It will be registered by the Cog or Client.
|
55
44
|
cmd = Command(callback=func, name=cmd_name, aliases=aliases or [], **attrs)
|
56
|
-
|
57
|
-
|
58
|
-
func.__command_object__ = cmd # type: ignore # type: ignore[attr-defined]
|
59
|
-
return func # Return the original function, now marked.
|
60
|
-
# Or return `cmd` if commands are registered globally immediately.
|
61
|
-
# For Cogs, returning `func` and letting Cog collect is cleaner.
|
45
|
+
func.__command_object__ = cmd # type: ignore
|
46
|
+
return func
|
62
47
|
|
63
48
|
return decorator
|
64
49
|
|
@@ -68,11 +53,6 @@ def listener(
|
|
68
53
|
) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
|
69
54
|
"""
|
70
55
|
A decorator that marks a function as an event listener within a Cog.
|
71
|
-
The actual registration happens when the Cog is added to the client.
|
72
|
-
|
73
|
-
Args:
|
74
|
-
name (Optional[str]): The name of the event to listen to.
|
75
|
-
Defaults to the function name (e.g., `on_message`).
|
76
56
|
"""
|
77
57
|
|
78
58
|
def decorator(
|
@@ -81,13 +61,11 @@ def listener(
|
|
81
61
|
if not asyncio.iscoroutinefunction(func):
|
82
62
|
raise TypeError("Listener callback must be a coroutine function.")
|
83
63
|
|
84
|
-
# 'name' here is from the outer 'listener' scope (closure)
|
85
64
|
actual_event_name = name or func.__name__
|
86
|
-
# Store listener info on the function for Cog to collect
|
87
65
|
setattr(func, "__listener_name__", actual_event_name)
|
88
66
|
return func
|
89
67
|
|
90
|
-
return decorator
|
68
|
+
return decorator
|
91
69
|
|
92
70
|
|
93
71
|
def check(
|
@@ -148,3 +126,67 @@ def cooldown(
|
|
148
126
|
return True
|
149
127
|
|
150
128
|
return check(predicate)
|
129
|
+
|
130
|
+
|
131
|
+
def _compute_permissions(
|
132
|
+
member: "Member", channel: "Channel", guild: "Guild"
|
133
|
+
) -> "Permissions":
|
134
|
+
"""Compute the effective permissions for a member in a channel."""
|
135
|
+
return channel.permissions_for(member)
|
136
|
+
|
137
|
+
|
138
|
+
def requires_permissions(
|
139
|
+
*perms: "Permissions",
|
140
|
+
) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
|
141
|
+
"""Check that the invoking member has the given permissions in the channel."""
|
142
|
+
|
143
|
+
async def predicate(ctx: "CommandContext") -> bool:
|
144
|
+
from .errors import CheckFailure
|
145
|
+
from disagreement.permissions import (
|
146
|
+
has_permissions,
|
147
|
+
missing_permissions,
|
148
|
+
)
|
149
|
+
from disagreement.models import Member
|
150
|
+
|
151
|
+
channel = getattr(ctx, "channel", None)
|
152
|
+
if channel is None and hasattr(ctx.bot, "get_channel"):
|
153
|
+
channel = ctx.bot.get_channel(ctx.message.channel_id)
|
154
|
+
if channel is None and hasattr(ctx.bot, "fetch_channel"):
|
155
|
+
channel = await ctx.bot.fetch_channel(ctx.message.channel_id)
|
156
|
+
|
157
|
+
if channel is None:
|
158
|
+
raise CheckFailure("Channel for permission check not found.")
|
159
|
+
|
160
|
+
guild = getattr(channel, "guild", None)
|
161
|
+
if not guild and hasattr(channel, "guild_id") and channel.guild_id:
|
162
|
+
if hasattr(ctx.bot, "get_guild"):
|
163
|
+
guild = ctx.bot.get_guild(channel.guild_id)
|
164
|
+
if not guild and hasattr(ctx.bot, "fetch_guild"):
|
165
|
+
guild = await ctx.bot.fetch_guild(channel.guild_id)
|
166
|
+
|
167
|
+
if not guild:
|
168
|
+
is_dm = not hasattr(channel, "guild_id") or not channel.guild_id
|
169
|
+
if is_dm:
|
170
|
+
if perms:
|
171
|
+
raise CheckFailure("Permission checks are not supported in DMs.")
|
172
|
+
return True
|
173
|
+
raise CheckFailure("Guild for permission check not found.")
|
174
|
+
|
175
|
+
member = ctx.author
|
176
|
+
if not isinstance(member, Member):
|
177
|
+
member = guild.get_member(ctx.author.id)
|
178
|
+
if not member and hasattr(ctx.bot, "fetch_member"):
|
179
|
+
member = await ctx.bot.fetch_member(guild.id, ctx.author.id)
|
180
|
+
|
181
|
+
if not member:
|
182
|
+
raise CheckFailure("Could not resolve author to a guild member.")
|
183
|
+
|
184
|
+
perms_value = _compute_permissions(member, channel, guild)
|
185
|
+
|
186
|
+
if not has_permissions(perms_value, *perms):
|
187
|
+
missing = missing_permissions(perms_value, *perms)
|
188
|
+
missing_names = ", ".join(p.name for p in missing if p.name)
|
189
|
+
raise CheckFailure(f"Missing permissions: {missing_names}")
|
190
|
+
return True
|
191
|
+
|
192
|
+
return check(predicate)
|
disagreement/ext/loader.py
CHANGED
@@ -5,7 +5,7 @@ import sys
|
|
5
5
|
from types import ModuleType
|
6
6
|
from typing import Dict
|
7
7
|
|
8
|
-
__all__ = ["load_extension", "unload_extension"]
|
8
|
+
__all__ = ["load_extension", "unload_extension", "reload_extension"]
|
9
9
|
|
10
10
|
_loaded_extensions: Dict[str, ModuleType] = {}
|
11
11
|
|
@@ -41,3 +41,14 @@ def unload_extension(name: str) -> None:
|
|
41
41
|
module.teardown()
|
42
42
|
|
43
43
|
sys.modules.pop(name, None)
|
44
|
+
|
45
|
+
|
46
|
+
def reload_extension(name: str) -> ModuleType:
|
47
|
+
"""Reload an extension by name.
|
48
|
+
|
49
|
+
This is a convenience wrapper around :func:`unload_extension` followed by
|
50
|
+
:func:`load_extension`.
|
51
|
+
"""
|
52
|
+
|
53
|
+
unload_extension(name)
|
54
|
+
return load_extension(name)
|