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.
@@ -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 dispatch(self, event_name: str, raw_data: Dict[str, Any]):
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
- parsed_data: Any = raw_data
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: # Listener takes no arguments
255
+ if num_params == 0:
209
256
  await listener()
210
- elif (
211
- num_params == 1
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 {event_name_upper} has an unhandled number of parameters ({num_params}). Skipping or attempting with one arg."
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: # Try with one arg if it takes any
224
- await listener(parsed_data)
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(event_name_upper, e, listener)
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 {event_name_upper}: {e}"
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(event_name_upper, e, listener)
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)
@@ -44,3 +44,5 @@ __all__ = [
44
44
  "OptionMetadata",
45
45
  "AppCommandContext", # To be defined
46
46
  ]
47
+
48
+ from .hybrid import *
@@ -1,58 +1,25 @@
1
1
  # disagreement/ext/app_commands/commands.py
2
2
 
3
3
  import inspect
4
- from typing import Callable, Optional, List, Dict, Any, Union, TYPE_CHECKING
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
- 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
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 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
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:
@@ -26,8 +26,8 @@ from .commands import (
26
26
  MessageCommand,
27
27
  AppCommand,
28
28
  AppCommandGroup,
29
- HybridCommand,
30
29
  )
30
+ from .hybrid import HybridCommand
31
31
  from disagreement.interactions import (
32
32
  ApplicationCommandOption,
33
33
  ApplicationCommandOptionChoice,
@@ -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 command, listener, check, check_any, cooldown
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 # For type hinting return or internal use
10
-
11
- # from .cog import Cog # For Cog specific decorators
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
- # We can attach the command object to the function, so Cogs can find it.
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 # This must be correctly indented under 'listener'
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)
@@ -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)