disagreement 0.2.0rc1__py3-none-any.whl → 0.4.0__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.
Files changed (38) hide show
  1. disagreement/__init__.py +2 -4
  2. disagreement/audio.py +42 -5
  3. disagreement/cache.py +43 -4
  4. disagreement/caching.py +121 -0
  5. disagreement/client.py +1682 -1535
  6. disagreement/enums.py +10 -3
  7. disagreement/error_handler.py +5 -1
  8. disagreement/errors.py +1341 -3
  9. disagreement/event_dispatcher.py +3 -5
  10. disagreement/ext/__init__.py +1 -0
  11. disagreement/ext/app_commands/__init__.py +0 -2
  12. disagreement/ext/app_commands/commands.py +0 -2
  13. disagreement/ext/app_commands/context.py +0 -2
  14. disagreement/ext/app_commands/converters.py +2 -4
  15. disagreement/ext/app_commands/decorators.py +5 -7
  16. disagreement/ext/app_commands/handler.py +1 -3
  17. disagreement/ext/app_commands/hybrid.py +0 -2
  18. disagreement/ext/commands/__init__.py +63 -61
  19. disagreement/ext/commands/cog.py +0 -2
  20. disagreement/ext/commands/converters.py +16 -5
  21. disagreement/ext/commands/core.py +728 -563
  22. disagreement/ext/commands/decorators.py +294 -219
  23. disagreement/ext/commands/errors.py +0 -2
  24. disagreement/ext/commands/help.py +0 -2
  25. disagreement/ext/commands/view.py +1 -3
  26. disagreement/gateway.py +632 -586
  27. disagreement/http.py +1362 -1041
  28. disagreement/interactions.py +0 -2
  29. disagreement/models.py +2682 -2263
  30. disagreement/shard_manager.py +0 -2
  31. disagreement/ui/view.py +167 -165
  32. disagreement/voice_client.py +263 -162
  33. {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/METADATA +33 -6
  34. disagreement-0.4.0.dist-info/RECORD +55 -0
  35. disagreement-0.2.0rc1.dist-info/RECORD +0 -54
  36. {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/WHEEL +0 -0
  37. {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/licenses/LICENSE +0 -0
  38. {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/top_level.txt +0 -0
@@ -1,219 +1,294 @@
1
- # disagreement/ext/commands/decorators.py
2
- from __future__ import annotations
3
-
4
- import asyncio
5
- import inspect
6
- import time
7
- from typing import Callable, Any, Optional, List, TYPE_CHECKING, Awaitable
8
-
9
- if TYPE_CHECKING:
10
- from .core import Command, CommandContext
11
- from disagreement.permissions import Permissions
12
- from disagreement.models import Member, Guild, Channel
13
-
14
-
15
- def command(
16
- name: Optional[str] = None, aliases: Optional[List[str]] = None, **attrs: Any
17
- ) -> Callable:
18
- """
19
- A decorator that transforms a function into a Command.
20
-
21
- Args:
22
- name (Optional[str]): The name of the command. Defaults to the function name.
23
- aliases (Optional[List[str]]): Alternative names for the command.
24
- **attrs: Additional attributes to pass to the Command constructor
25
- (e.g., brief, description, hidden).
26
-
27
- Returns:
28
- Callable: A decorator that registers the command.
29
- """
30
-
31
- def decorator(
32
- func: Callable[..., Awaitable[None]],
33
- ) -> Callable[..., Awaitable[None]]:
34
- if not asyncio.iscoroutinefunction(func):
35
- raise TypeError("Command callback must be a coroutine function.")
36
-
37
- from .core import Command
38
-
39
- cmd_name = name or func.__name__
40
-
41
- if hasattr(func, "__command_attrs__"):
42
- raise TypeError("Function is already a command or has command attributes.")
43
-
44
- cmd = Command(callback=func, name=cmd_name, aliases=aliases or [], **attrs)
45
- func.__command_object__ = cmd # type: ignore
46
- return func
47
-
48
- return decorator
49
-
50
-
51
- def listener(
52
- name: Optional[str] = None,
53
- ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
54
- """
55
- A decorator that marks a function as an event listener within a Cog.
56
- """
57
-
58
- def decorator(
59
- func: Callable[..., Awaitable[None]],
60
- ) -> Callable[..., Awaitable[None]]:
61
- if not asyncio.iscoroutinefunction(func):
62
- raise TypeError("Listener callback must be a coroutine function.")
63
-
64
- actual_event_name = name or func.__name__
65
- setattr(func, "__listener_name__", actual_event_name)
66
- return func
67
-
68
- return decorator
69
-
70
-
71
- def check(
72
- predicate: Callable[["CommandContext"], Awaitable[bool] | bool],
73
- ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
74
- """Decorator to add a check to a command."""
75
-
76
- def decorator(
77
- func: Callable[..., Awaitable[None]],
78
- ) -> Callable[..., Awaitable[None]]:
79
- checks = getattr(func, "__command_checks__", [])
80
- checks.append(predicate)
81
- setattr(func, "__command_checks__", checks)
82
- return func
83
-
84
- return decorator
85
-
86
-
87
- def check_any(
88
- *predicates: Callable[["CommandContext"], Awaitable[bool] | bool]
89
- ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
90
- """Decorator that passes if any predicate returns ``True``."""
91
-
92
- async def predicate(ctx: "CommandContext") -> bool:
93
- from .errors import CheckAnyFailure, CheckFailure
94
-
95
- errors = []
96
- for p in predicates:
97
- try:
98
- result = p(ctx)
99
- if inspect.isawaitable(result):
100
- result = await result
101
- if result:
102
- return True
103
- except CheckFailure as e:
104
- errors.append(e)
105
- raise CheckAnyFailure(errors)
106
-
107
- return check(predicate)
108
-
109
-
110
- def max_concurrency(
111
- number: int, per: str = "user"
112
- ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
113
- """Limit how many concurrent invocations of a command are allowed.
114
-
115
- Parameters
116
- ----------
117
- number:
118
- The maximum number of concurrent invocations.
119
- per:
120
- The scope of the limiter. Can be ``"user"``, ``"guild"`` or ``"global"``.
121
- """
122
-
123
- if number < 1:
124
- raise ValueError("Concurrency number must be at least 1.")
125
- if per not in {"user", "guild", "global"}:
126
- raise ValueError("per must be 'user', 'guild', or 'global'.")
127
-
128
- def decorator(
129
- func: Callable[..., Awaitable[None]],
130
- ) -> Callable[..., Awaitable[None]]:
131
- setattr(func, "__max_concurrency__", (number, per))
132
- return func
133
-
134
- return decorator
135
-
136
-
137
- def cooldown(
138
- rate: int, per: float
139
- ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
140
- """Simple per-user cooldown decorator."""
141
-
142
- buckets: dict[str, dict[str, float]] = {}
143
-
144
- async def predicate(ctx: "CommandContext") -> bool:
145
- from .errors import CommandOnCooldown
146
-
147
- now = time.monotonic()
148
- user_buckets = buckets.setdefault(ctx.command.name, {})
149
- reset = user_buckets.get(ctx.author.id, 0)
150
- if now < reset:
151
- raise CommandOnCooldown(reset - now)
152
- user_buckets[ctx.author.id] = now + per
153
- return True
154
-
155
- return check(predicate)
156
-
157
-
158
- def _compute_permissions(
159
- member: "Member", channel: "Channel", guild: "Guild"
160
- ) -> "Permissions":
161
- """Compute the effective permissions for a member in a channel."""
162
- return channel.permissions_for(member)
163
-
164
-
165
- def requires_permissions(
166
- *perms: "Permissions",
167
- ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
168
- """Check that the invoking member has the given permissions in the channel."""
169
-
170
- async def predicate(ctx: "CommandContext") -> bool:
171
- from .errors import CheckFailure
172
- from disagreement.permissions import (
173
- has_permissions,
174
- missing_permissions,
175
- )
176
- from disagreement.models import Member
177
-
178
- channel = getattr(ctx, "channel", None)
179
- if channel is None and hasattr(ctx.bot, "get_channel"):
180
- channel = ctx.bot.get_channel(ctx.message.channel_id)
181
- if channel is None and hasattr(ctx.bot, "fetch_channel"):
182
- channel = await ctx.bot.fetch_channel(ctx.message.channel_id)
183
-
184
- if channel is None:
185
- raise CheckFailure("Channel for permission check not found.")
186
-
187
- guild = getattr(channel, "guild", None)
188
- if not guild and hasattr(channel, "guild_id") and channel.guild_id:
189
- if hasattr(ctx.bot, "get_guild"):
190
- guild = ctx.bot.get_guild(channel.guild_id)
191
- if not guild and hasattr(ctx.bot, "fetch_guild"):
192
- guild = await ctx.bot.fetch_guild(channel.guild_id)
193
-
194
- if not guild:
195
- is_dm = not hasattr(channel, "guild_id") or not channel.guild_id
196
- if is_dm:
197
- if perms:
198
- raise CheckFailure("Permission checks are not supported in DMs.")
199
- return True
200
- raise CheckFailure("Guild for permission check not found.")
201
-
202
- member = ctx.author
203
- if not isinstance(member, Member):
204
- member = guild.get_member(ctx.author.id)
205
- if not member and hasattr(ctx.bot, "fetch_member"):
206
- member = await ctx.bot.fetch_member(guild.id, ctx.author.id)
207
-
208
- if not member:
209
- raise CheckFailure("Could not resolve author to a guild member.")
210
-
211
- perms_value = _compute_permissions(member, channel, guild)
212
-
213
- if not has_permissions(perms_value, *perms):
214
- missing = missing_permissions(perms_value, *perms)
215
- missing_names = ", ".join(p.name for p in missing if p.name)
216
- raise CheckFailure(f"Missing permissions: {missing_names}")
217
- return True
218
-
219
- return check(predicate)
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import time
6
+ from typing import Callable, Any, Optional, List, TYPE_CHECKING, Awaitable
7
+
8
+ if TYPE_CHECKING:
9
+ from .core import Command, CommandContext
10
+ from disagreement.permissions import Permissions
11
+ from disagreement.models import Member, Guild, Channel
12
+
13
+
14
+ def command(
15
+ name: Optional[str] = None, aliases: Optional[List[str]] = None, **attrs: Any
16
+ ) -> Callable:
17
+ """
18
+ A decorator that transforms a function into a Command.
19
+
20
+ Args:
21
+ name (Optional[str]): The name of the command. Defaults to the function name.
22
+ aliases (Optional[List[str]]): Alternative names for the command.
23
+ **attrs: Additional attributes to pass to the Command constructor
24
+ (e.g., brief, description, hidden).
25
+
26
+ Returns:
27
+ Callable: A decorator that registers the command.
28
+ """
29
+
30
+ def decorator(
31
+ func: Callable[..., Awaitable[None]],
32
+ ) -> Callable[..., Awaitable[None]]:
33
+ if not asyncio.iscoroutinefunction(func):
34
+ raise TypeError("Command callback must be a coroutine function.")
35
+
36
+ from .core import Command
37
+
38
+ cmd_name = name or func.__name__
39
+
40
+ if hasattr(func, "__command_attrs__"):
41
+ raise TypeError("Function is already a command or has command attributes.")
42
+
43
+ cmd = Command(callback=func, name=cmd_name, aliases=aliases or [], **attrs)
44
+ func.__command_object__ = cmd # type: ignore
45
+ return func
46
+
47
+ return decorator
48
+
49
+
50
+ def listener(
51
+ name: Optional[str] = None,
52
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
53
+ """
54
+ A decorator that marks a function as an event listener within a Cog.
55
+ """
56
+
57
+ def decorator(
58
+ func: Callable[..., Awaitable[None]],
59
+ ) -> Callable[..., Awaitable[None]]:
60
+ if not asyncio.iscoroutinefunction(func):
61
+ raise TypeError("Listener callback must be a coroutine function.")
62
+
63
+ actual_event_name = name or func.__name__
64
+ setattr(func, "__listener_name__", actual_event_name)
65
+ return func
66
+
67
+ return decorator
68
+
69
+
70
+ def check(
71
+ predicate: Callable[["CommandContext"], Awaitable[bool] | bool],
72
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
73
+ """Decorator to add a check to a command."""
74
+
75
+ def decorator(
76
+ func: Callable[..., Awaitable[None]],
77
+ ) -> Callable[..., Awaitable[None]]:
78
+ checks = getattr(func, "__command_checks__", [])
79
+ checks.append(predicate)
80
+ setattr(func, "__command_checks__", checks)
81
+ return func
82
+
83
+ return decorator
84
+
85
+
86
+ def check_any(
87
+ *predicates: Callable[["CommandContext"], Awaitable[bool] | bool]
88
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
89
+ """Decorator that passes if any predicate returns ``True``."""
90
+
91
+ async def predicate(ctx: "CommandContext") -> bool:
92
+ from .errors import CheckAnyFailure, CheckFailure
93
+
94
+ errors = []
95
+ for p in predicates:
96
+ try:
97
+ result = p(ctx)
98
+ if inspect.isawaitable(result):
99
+ result = await result
100
+ if result:
101
+ return True
102
+ except CheckFailure as e:
103
+ errors.append(e)
104
+ raise CheckAnyFailure(errors)
105
+
106
+ return check(predicate)
107
+
108
+
109
+ def max_concurrency(
110
+ number: int, per: str = "user"
111
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
112
+ """Limit how many concurrent invocations of a command are allowed.
113
+
114
+ Parameters
115
+ ----------
116
+ number:
117
+ The maximum number of concurrent invocations.
118
+ per:
119
+ The scope of the limiter. Can be ``"user"``, ``"guild"`` or ``"global"``.
120
+ """
121
+
122
+ if number < 1:
123
+ raise ValueError("Concurrency number must be at least 1.")
124
+ if per not in {"user", "guild", "global"}:
125
+ raise ValueError("per must be 'user', 'guild', or 'global'.")
126
+
127
+ def decorator(
128
+ func: Callable[..., Awaitable[None]],
129
+ ) -> Callable[..., Awaitable[None]]:
130
+ setattr(func, "__max_concurrency__", (number, per))
131
+ return func
132
+
133
+ return decorator
134
+
135
+
136
+ def cooldown(
137
+ rate: int, per: float
138
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
139
+ """Simple per-user cooldown decorator."""
140
+
141
+ buckets: dict[str, dict[str, float]] = {}
142
+
143
+ async def predicate(ctx: "CommandContext") -> bool:
144
+ from .errors import CommandOnCooldown
145
+
146
+ now = time.monotonic()
147
+ user_buckets = buckets.setdefault(ctx.command.name, {})
148
+ reset = user_buckets.get(ctx.author.id, 0)
149
+ if now < reset:
150
+ raise CommandOnCooldown(reset - now)
151
+ user_buckets[ctx.author.id] = now + per
152
+ return True
153
+
154
+ return check(predicate)
155
+
156
+
157
+ def _compute_permissions(
158
+ member: "Member", channel: "Channel", guild: "Guild"
159
+ ) -> "Permissions":
160
+ """Compute the effective permissions for a member in a channel."""
161
+ return channel.permissions_for(member)
162
+
163
+
164
+ def requires_permissions(
165
+ *perms: "Permissions",
166
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
167
+ """Check that the invoking member has the given permissions in the channel."""
168
+
169
+ async def predicate(ctx: "CommandContext") -> bool:
170
+ from .errors import CheckFailure
171
+ from disagreement.permissions import (
172
+ has_permissions,
173
+ missing_permissions,
174
+ )
175
+ from disagreement.models import Member
176
+
177
+ channel = getattr(ctx, "channel", None)
178
+ if channel is None and hasattr(ctx.bot, "get_channel"):
179
+ channel = ctx.bot.get_channel(ctx.message.channel_id)
180
+ if channel is None and hasattr(ctx.bot, "fetch_channel"):
181
+ channel = await ctx.bot.fetch_channel(ctx.message.channel_id)
182
+
183
+ if channel is None:
184
+ raise CheckFailure("Channel for permission check not found.")
185
+
186
+ guild = getattr(channel, "guild", None)
187
+ if not guild and hasattr(channel, "guild_id") and channel.guild_id:
188
+ if hasattr(ctx.bot, "get_guild"):
189
+ guild = ctx.bot.get_guild(channel.guild_id)
190
+ if not guild and hasattr(ctx.bot, "fetch_guild"):
191
+ guild = await ctx.bot.fetch_guild(channel.guild_id)
192
+
193
+ if not guild:
194
+ is_dm = not hasattr(channel, "guild_id") or not channel.guild_id
195
+ if is_dm:
196
+ if perms:
197
+ raise CheckFailure("Permission checks are not supported in DMs.")
198
+ return True
199
+ raise CheckFailure("Guild for permission check not found.")
200
+
201
+ member = ctx.author
202
+ if not isinstance(member, Member):
203
+ member = guild.get_member(ctx.author.id)
204
+ if not member and hasattr(ctx.bot, "fetch_member"):
205
+ member = await ctx.bot.fetch_member(guild.id, ctx.author.id)
206
+
207
+ if not member:
208
+ raise CheckFailure("Could not resolve author to a guild member.")
209
+
210
+ perms_value = _compute_permissions(member, channel, guild)
211
+
212
+ if not has_permissions(perms_value, *perms):
213
+ missing = missing_permissions(perms_value, *perms)
214
+ missing_names = ", ".join(p.name for p in missing if p.name)
215
+ raise CheckFailure(f"Missing permissions: {missing_names}")
216
+ return True
217
+
218
+ return check(predicate)
219
+
220
+
221
+ def has_role(
222
+ name_or_id: str | int,
223
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
224
+ """Check that the invoking member has a role with the given name or ID."""
225
+
226
+ async def predicate(ctx: "CommandContext") -> bool:
227
+ from .errors import CheckFailure
228
+ from disagreement.models import Member
229
+
230
+ if not ctx.guild:
231
+ raise CheckFailure("This command cannot be used in DMs.")
232
+
233
+ author = ctx.author
234
+ if not isinstance(author, Member):
235
+ try:
236
+ author = await ctx.bot.fetch_member(ctx.guild.id, author.id)
237
+ except Exception:
238
+ raise CheckFailure("Could not resolve author to a guild member.")
239
+
240
+ if not author:
241
+ raise CheckFailure("Could not resolve author to a guild member.")
242
+
243
+ # Create a list of the member's role objects by looking them up in the guild's roles list
244
+ member_roles = [role for role in ctx.guild.roles if role.id in author.roles]
245
+
246
+ if any(
247
+ role.id == str(name_or_id) or role.name == name_or_id
248
+ for role in member_roles
249
+ ):
250
+ return True
251
+
252
+ raise CheckFailure(f"You need the '{name_or_id}' role to use this command.")
253
+
254
+ return check(predicate)
255
+
256
+
257
+ def has_any_role(
258
+ *names_or_ids: str | int,
259
+ ) -> Callable[[Callable[..., Awaitable[None]]], Callable[..., Awaitable[None]]]:
260
+ """Check that the invoking member has any of the roles with the given names or IDs."""
261
+
262
+ async def predicate(ctx: "CommandContext") -> bool:
263
+ from .errors import CheckFailure
264
+ from disagreement.models import Member
265
+
266
+ if not ctx.guild:
267
+ raise CheckFailure("This command cannot be used in DMs.")
268
+
269
+ author = ctx.author
270
+ if not isinstance(author, Member):
271
+ try:
272
+ author = await ctx.bot.fetch_member(ctx.guild.id, author.id)
273
+ except Exception:
274
+ raise CheckFailure("Could not resolve author to a guild member.")
275
+
276
+ if not author:
277
+ raise CheckFailure("Could not resolve author to a guild member.")
278
+
279
+ member_roles = [role for role in ctx.guild.roles if role.id in author.roles]
280
+ # Convert names_or_ids to a set for efficient lookup
281
+ names_or_ids_set = set(map(str, names_or_ids))
282
+
283
+ if any(
284
+ role.id in names_or_ids_set or role.name in names_or_ids_set
285
+ for role in member_roles
286
+ ):
287
+ return True
288
+
289
+ role_list = ", ".join(f"'{r}'" for r in names_or_ids)
290
+ raise CheckFailure(
291
+ f"You need one of the following roles to use this command: {role_list}"
292
+ )
293
+
294
+ return check(predicate)
@@ -1,5 +1,3 @@
1
- # disagreement/ext/commands/errors.py
2
-
3
1
  """
4
2
  Custom exceptions for the command extension.
5
3
  """
@@ -1,5 +1,3 @@
1
- # disagreement/ext/commands/help.py
2
-
3
1
  from typing import List, Optional
4
2
 
5
3
  from .core import Command, CommandContext, CommandHandler
@@ -1,5 +1,3 @@
1
- # disagreement/ext/commands/view.py
2
-
3
1
  import re
4
2
 
5
3
 
@@ -47,7 +45,7 @@ class StringView:
47
45
  word = match.group(0)
48
46
  self.index += len(word)
49
47
  return word
50
- return "" # Should not happen if not eof and skip_whitespace was called
48
+ return ""
51
49
 
52
50
  def get_quoted_string(self) -> str:
53
51
  """