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.
- disagreement/__init__.py +2 -4
- disagreement/audio.py +42 -5
- disagreement/cache.py +43 -4
- disagreement/caching.py +121 -0
- disagreement/client.py +1682 -1535
- disagreement/enums.py +10 -3
- disagreement/error_handler.py +5 -1
- disagreement/errors.py +1341 -3
- disagreement/event_dispatcher.py +3 -5
- disagreement/ext/__init__.py +1 -0
- disagreement/ext/app_commands/__init__.py +0 -2
- disagreement/ext/app_commands/commands.py +0 -2
- disagreement/ext/app_commands/context.py +0 -2
- disagreement/ext/app_commands/converters.py +2 -4
- disagreement/ext/app_commands/decorators.py +5 -7
- disagreement/ext/app_commands/handler.py +1 -3
- disagreement/ext/app_commands/hybrid.py +0 -2
- disagreement/ext/commands/__init__.py +63 -61
- disagreement/ext/commands/cog.py +0 -2
- disagreement/ext/commands/converters.py +16 -5
- disagreement/ext/commands/core.py +728 -563
- disagreement/ext/commands/decorators.py +294 -219
- disagreement/ext/commands/errors.py +0 -2
- disagreement/ext/commands/help.py +0 -2
- disagreement/ext/commands/view.py +1 -3
- disagreement/gateway.py +632 -586
- disagreement/http.py +1362 -1041
- disagreement/interactions.py +0 -2
- disagreement/models.py +2682 -2263
- disagreement/shard_manager.py +0 -2
- disagreement/ui/view.py +167 -165
- disagreement/voice_client.py +263 -162
- {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/METADATA +33 -6
- disagreement-0.4.0.dist-info/RECORD +55 -0
- disagreement-0.2.0rc1.dist-info/RECORD +0 -54
- {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/WHEEL +0 -0
- {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.2.0rc1.dist-info → disagreement-0.4.0.dist-info}/top_level.txt +0 -0
@@ -1,219 +1,294 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
import
|
5
|
-
import
|
6
|
-
import
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
from .
|
11
|
-
from disagreement.
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
func
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
checks
|
80
|
-
checks
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
from .
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
channel
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
guild
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
is_dm
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
member
|
203
|
-
|
204
|
-
member
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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/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 ""
|
48
|
+
return ""
|
51
49
|
|
52
50
|
def get_quoted_string(self) -> str:
|
53
51
|
"""
|