disagreement 0.1.0rc1__py3-none-any.whl → 0.1.0rc3__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 +9 -5
- disagreement/client.py +41 -6
- disagreement/color.py +28 -0
- disagreement/enums.py +5 -0
- 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/handler.py +25 -12
- disagreement/ext/app_commands/hybrid.py +61 -0
- disagreement/ext/commands/cog.py +15 -6
- disagreement/ext/commands/core.py +28 -12
- disagreement/ext/tasks.py +46 -0
- disagreement/gateway.py +102 -63
- disagreement/http.py +134 -17
- disagreement/interactions.py +17 -14
- disagreement/models.py +124 -9
- disagreement/py.typed +0 -0
- disagreement/ui/modal.py +1 -1
- disagreement/utils.py +63 -0
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/METADATA +6 -5
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/RECORD +24 -22
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/WHEEL +0 -0
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.1.0rc1.dist-info → disagreement-0.1.0rc3.dist-info}/top_level.txt +0 -0
disagreement/http.py
CHANGED
@@ -5,6 +5,7 @@ HTTP client for interacting with the Discord REST API.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import asyncio
|
8
|
+
import logging
|
8
9
|
import aiohttp # pylint: disable=import-error
|
9
10
|
import json
|
10
11
|
from urllib.parse import quote
|
@@ -17,15 +18,19 @@ from .errors import (
|
|
17
18
|
DisagreementException,
|
18
19
|
)
|
19
20
|
from . import __version__ # For User-Agent
|
21
|
+
from .rate_limiter import RateLimiter
|
22
|
+
from .interactions import InteractionResponsePayload
|
20
23
|
|
21
24
|
if TYPE_CHECKING:
|
22
25
|
from .client import Client
|
23
26
|
from .models import Message, Webhook, File
|
24
|
-
from .interactions import ApplicationCommand,
|
27
|
+
from .interactions import ApplicationCommand, Snowflake
|
25
28
|
|
26
29
|
# Discord API constants
|
27
30
|
API_BASE_URL = "https://discord.com/api/v10" # Using API v10
|
28
31
|
|
32
|
+
logger = logging.getLogger(__name__)
|
33
|
+
|
29
34
|
|
30
35
|
class HTTPClient:
|
31
36
|
"""Handles HTTP requests to the Discord API."""
|
@@ -44,8 +49,7 @@ class HTTPClient:
|
|
44
49
|
|
45
50
|
self.verbose = verbose
|
46
51
|
|
47
|
-
self.
|
48
|
-
self._global_rate_limit_lock.set() # Initially unlocked
|
52
|
+
self._rate_limiter = RateLimiter()
|
49
53
|
|
50
54
|
async def _ensure_session(self):
|
51
55
|
if self._session is None or self._session.closed:
|
@@ -85,12 +89,18 @@ class HTTPClient:
|
|
85
89
|
final_headers.update(custom_headers)
|
86
90
|
|
87
91
|
if self.verbose:
|
88
|
-
|
92
|
+
logger.debug(
|
93
|
+
"HTTP REQUEST: %s %s | payload=%s params=%s",
|
94
|
+
method,
|
95
|
+
url,
|
96
|
+
payload,
|
97
|
+
params,
|
98
|
+
)
|
89
99
|
|
90
|
-
|
91
|
-
await self._global_rate_limit_lock.wait()
|
100
|
+
route = f"{method.upper()}:{endpoint}"
|
92
101
|
|
93
102
|
for attempt in range(5): # Max 5 retries for rate limits
|
103
|
+
await self._rate_limiter.acquire(route)
|
94
104
|
assert self._session is not None, "ClientSession not initialized"
|
95
105
|
async with self._session.request(
|
96
106
|
method,
|
@@ -118,7 +128,11 @@ class HTTPClient:
|
|
118
128
|
) # Fallback to text if JSON parsing fails
|
119
129
|
|
120
130
|
if self.verbose:
|
121
|
-
|
131
|
+
logger.debug(
|
132
|
+
"HTTP RESPONSE: %s %s | %s", response.status, url, data
|
133
|
+
)
|
134
|
+
|
135
|
+
self._rate_limiter.release(route, response.headers)
|
122
136
|
|
123
137
|
if 200 <= response.status < 300:
|
124
138
|
if response.status == 204:
|
@@ -142,16 +156,17 @@ class HTTPClient:
|
|
142
156
|
if data and isinstance(data, dict) and "message" in data:
|
143
157
|
error_message += f" Discord says: {data['message']}"
|
144
158
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
self._global_rate_limit_lock.set()
|
149
|
-
else:
|
150
|
-
await asyncio.sleep(retry_after)
|
159
|
+
await self._rate_limiter.handle_rate_limit(
|
160
|
+
route, retry_after, is_global
|
161
|
+
)
|
151
162
|
|
152
163
|
if attempt < 4: # Don't log on the last attempt before raising
|
153
|
-
|
154
|
-
|
164
|
+
logger.warning(
|
165
|
+
"%s Retrying after %ss (Attempt %s/5). Global: %s",
|
166
|
+
error_message,
|
167
|
+
retry_after,
|
168
|
+
attempt + 1,
|
169
|
+
is_global,
|
155
170
|
)
|
156
171
|
continue # Retry the request
|
157
172
|
else: # Last attempt failed
|
@@ -348,6 +363,16 @@ class HTTPClient:
|
|
348
363
|
f"/channels/{channel_id}/messages/{message_id}/reactions/{encoded}",
|
349
364
|
)
|
350
365
|
|
366
|
+
async def clear_reactions(
|
367
|
+
self, channel_id: "Snowflake", message_id: "Snowflake"
|
368
|
+
) -> None:
|
369
|
+
"""Removes all reactions from a message."""
|
370
|
+
|
371
|
+
await self.request(
|
372
|
+
"DELETE",
|
373
|
+
f"/channels/{channel_id}/messages/{message_id}/reactions",
|
374
|
+
)
|
375
|
+
|
351
376
|
async def bulk_delete_messages(
|
352
377
|
self, channel_id: "Snowflake", messages: List["Snowflake"]
|
353
378
|
) -> List["Snowflake"]:
|
@@ -411,6 +436,92 @@ class HTTPClient:
|
|
411
436
|
|
412
437
|
await self.request("DELETE", f"/webhooks/{webhook_id}")
|
413
438
|
|
439
|
+
async def execute_webhook(
|
440
|
+
self,
|
441
|
+
webhook_id: "Snowflake",
|
442
|
+
token: str,
|
443
|
+
*,
|
444
|
+
content: Optional[str] = None,
|
445
|
+
tts: bool = False,
|
446
|
+
embeds: Optional[List[Dict[str, Any]]] = None,
|
447
|
+
components: Optional[List[Dict[str, Any]]] = None,
|
448
|
+
allowed_mentions: Optional[dict] = None,
|
449
|
+
attachments: Optional[List[Any]] = None,
|
450
|
+
files: Optional[List[Any]] = None,
|
451
|
+
flags: Optional[int] = None,
|
452
|
+
username: Optional[str] = None,
|
453
|
+
avatar_url: Optional[str] = None,
|
454
|
+
) -> Dict[str, Any]:
|
455
|
+
"""Executes a webhook and returns the created message."""
|
456
|
+
|
457
|
+
payload: Dict[str, Any] = {}
|
458
|
+
if content is not None:
|
459
|
+
payload["content"] = content
|
460
|
+
if tts:
|
461
|
+
payload["tts"] = True
|
462
|
+
if embeds:
|
463
|
+
payload["embeds"] = embeds
|
464
|
+
if components:
|
465
|
+
payload["components"] = components
|
466
|
+
if allowed_mentions:
|
467
|
+
payload["allowed_mentions"] = allowed_mentions
|
468
|
+
if username:
|
469
|
+
payload["username"] = username
|
470
|
+
if avatar_url:
|
471
|
+
payload["avatar_url"] = avatar_url
|
472
|
+
|
473
|
+
all_files: List["File"] = []
|
474
|
+
if attachments is not None:
|
475
|
+
payload["attachments"] = []
|
476
|
+
for a in attachments:
|
477
|
+
if hasattr(a, "data") and hasattr(a, "filename"):
|
478
|
+
idx = len(all_files)
|
479
|
+
all_files.append(a)
|
480
|
+
payload["attachments"].append({"id": idx, "filename": a.filename})
|
481
|
+
else:
|
482
|
+
payload["attachments"].append(
|
483
|
+
a.to_dict() if hasattr(a, "to_dict") else a
|
484
|
+
)
|
485
|
+
if files is not None:
|
486
|
+
for f in files:
|
487
|
+
if hasattr(f, "data") and hasattr(f, "filename"):
|
488
|
+
idx = len(all_files)
|
489
|
+
all_files.append(f)
|
490
|
+
if "attachments" not in payload:
|
491
|
+
payload["attachments"] = []
|
492
|
+
payload["attachments"].append({"id": idx, "filename": f.filename})
|
493
|
+
else:
|
494
|
+
raise TypeError("files must be File objects")
|
495
|
+
if flags:
|
496
|
+
payload["flags"] = flags
|
497
|
+
|
498
|
+
if all_files:
|
499
|
+
form = aiohttp.FormData()
|
500
|
+
form.add_field(
|
501
|
+
"payload_json", json.dumps(payload), content_type="application/json"
|
502
|
+
)
|
503
|
+
for idx, f in enumerate(all_files):
|
504
|
+
form.add_field(
|
505
|
+
f"files[{idx}]",
|
506
|
+
f.data,
|
507
|
+
filename=f.filename,
|
508
|
+
content_type="application/octet-stream",
|
509
|
+
)
|
510
|
+
return await self.request(
|
511
|
+
"POST",
|
512
|
+
f"/webhooks/{webhook_id}/{token}",
|
513
|
+
payload=form,
|
514
|
+
is_json=False,
|
515
|
+
use_auth_header=False,
|
516
|
+
)
|
517
|
+
|
518
|
+
return await self.request(
|
519
|
+
"POST",
|
520
|
+
f"/webhooks/{webhook_id}/{token}",
|
521
|
+
payload=payload,
|
522
|
+
use_auth_header=False,
|
523
|
+
)
|
524
|
+
|
414
525
|
async def get_user(self, user_id: "Snowflake") -> Dict[str, Any]:
|
415
526
|
"""Fetches a user object for a given user ID."""
|
416
527
|
return await self.request("GET", f"/users/{user_id}")
|
@@ -657,7 +768,7 @@ class HTTPClient:
|
|
657
768
|
self,
|
658
769
|
interaction_id: "Snowflake",
|
659
770
|
interaction_token: str,
|
660
|
-
payload: "InteractionResponsePayload",
|
771
|
+
payload: Union["InteractionResponsePayload", Dict[str, Any]],
|
661
772
|
*,
|
662
773
|
ephemeral: bool = False,
|
663
774
|
) -> None:
|
@@ -670,10 +781,16 @@ class HTTPClient:
|
|
670
781
|
"""
|
671
782
|
# Interaction responses do not use the bot token in the Authorization header.
|
672
783
|
# They are authenticated by the interaction_token in the URL.
|
784
|
+
payload_data: Dict[str, Any]
|
785
|
+
if isinstance(payload, InteractionResponsePayload):
|
786
|
+
payload_data = payload.to_dict()
|
787
|
+
else:
|
788
|
+
payload_data = payload
|
789
|
+
|
673
790
|
await self.request(
|
674
791
|
"POST",
|
675
792
|
f"/interactions/{interaction_id}/{interaction_token}/callback",
|
676
|
-
payload=
|
793
|
+
payload=payload_data,
|
677
794
|
use_auth_header=False,
|
678
795
|
)
|
679
796
|
|
disagreement/interactions.py
CHANGED
@@ -395,17 +395,14 @@ class Interaction:
|
|
395
395
|
|
396
396
|
async def respond_modal(self, modal: "Modal") -> None:
|
397
397
|
"""|coro| Send a modal in response to this interaction."""
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
"type": InteractionCallbackType.MODAL.value,
|
403
|
-
"data": modal.to_dict(),
|
404
|
-
}
|
398
|
+
payload = InteractionResponsePayload(
|
399
|
+
type=InteractionCallbackType.MODAL,
|
400
|
+
data=modal.to_dict(),
|
401
|
+
)
|
405
402
|
await self._client._http.create_interaction_response(
|
406
403
|
interaction_id=self.id,
|
407
404
|
interaction_token=self.token,
|
408
|
-
payload=
|
405
|
+
payload=payload,
|
409
406
|
)
|
410
407
|
|
411
408
|
async def edit(
|
@@ -489,7 +486,7 @@ class InteractionResponse:
|
|
489
486
|
"""Sends a modal response."""
|
490
487
|
payload = InteractionResponsePayload(
|
491
488
|
type=InteractionCallbackType.MODAL,
|
492
|
-
data=
|
489
|
+
data=modal.to_dict(),
|
493
490
|
)
|
494
491
|
await self._interaction._client._http.create_interaction_response(
|
495
492
|
self._interaction.id,
|
@@ -510,7 +507,7 @@ class InteractionCallbackData:
|
|
510
507
|
)
|
511
508
|
self.allowed_mentions: Optional[AllowedMentions] = (
|
512
509
|
AllowedMentions(data["allowed_mentions"])
|
513
|
-
if
|
510
|
+
if "allowed_mentions" in data
|
514
511
|
else None
|
515
512
|
)
|
516
513
|
self.flags: Optional[int] = data.get("flags") # MessageFlags enum could be used
|
@@ -557,16 +554,22 @@ class InteractionResponsePayload:
|
|
557
554
|
def __init__(
|
558
555
|
self,
|
559
556
|
type: InteractionCallbackType,
|
560
|
-
data: Optional[InteractionCallbackData] = None,
|
557
|
+
data: Optional[Union[InteractionCallbackData, Dict[str, Any]]] = None,
|
561
558
|
):
|
562
|
-
self.type
|
563
|
-
self.data
|
559
|
+
self.type = type
|
560
|
+
self.data = data
|
564
561
|
|
565
562
|
def to_dict(self) -> Dict[str, Any]:
|
566
563
|
payload: Dict[str, Any] = {"type": self.type.value}
|
567
564
|
if self.data:
|
568
|
-
|
565
|
+
if isinstance(self.data, dict):
|
566
|
+
payload["data"] = self.data
|
567
|
+
else:
|
568
|
+
payload["data"] = self.data.to_dict()
|
569
569
|
return payload
|
570
570
|
|
571
571
|
def __repr__(self) -> str:
|
572
572
|
return f"<InteractionResponsePayload type={self.type!r}>"
|
573
|
+
|
574
|
+
def __getitem__(self, key: str) -> Any:
|
575
|
+
return self.to_dict()[key]
|
disagreement/models.py
CHANGED
@@ -4,12 +4,12 @@
|
|
4
4
|
Data models for Discord objects.
|
5
5
|
"""
|
6
6
|
|
7
|
-
import json
|
8
|
-
import asyncio
|
9
|
-
import aiohttp # pylint: disable=import-error
|
10
7
|
import asyncio
|
11
|
-
|
8
|
+
import json
|
9
|
+
from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union
|
12
10
|
|
11
|
+
import aiohttp # pylint: disable=import-error
|
12
|
+
from .color import Color
|
13
13
|
from .errors import DisagreementException, HTTPException
|
14
14
|
from .enums import ( # These enums will need to be defined in disagreement/enums.py
|
15
15
|
VerificationLevel,
|
@@ -201,6 +201,21 @@ class Message:
|
|
201
201
|
view=view,
|
202
202
|
)
|
203
203
|
|
204
|
+
async def add_reaction(self, emoji: str) -> None:
|
205
|
+
"""|coro| Add a reaction to this message."""
|
206
|
+
|
207
|
+
await self._client.add_reaction(self.channel_id, self.id, emoji)
|
208
|
+
|
209
|
+
async def remove_reaction(self, emoji: str) -> None:
|
210
|
+
"""|coro| Remove the bot's reaction from this message."""
|
211
|
+
|
212
|
+
await self._client.remove_reaction(self.channel_id, self.id, emoji)
|
213
|
+
|
214
|
+
async def clear_reactions(self) -> None:
|
215
|
+
"""|coro| Remove all reactions from this message."""
|
216
|
+
|
217
|
+
await self._client.clear_reactions(self.channel_id, self.id)
|
218
|
+
|
204
219
|
async def delete(self, delay: Optional[float] = None) -> None:
|
205
220
|
"""|coro|
|
206
221
|
|
@@ -312,7 +327,7 @@ class Embed:
|
|
312
327
|
self.description: Optional[str] = data.get("description")
|
313
328
|
self.url: Optional[str] = data.get("url")
|
314
329
|
self.timestamp: Optional[str] = data.get("timestamp") # ISO8601 timestamp
|
315
|
-
self.color
|
330
|
+
self.color = Color.parse(data.get("color"))
|
316
331
|
|
317
332
|
self.footer: Optional[EmbedFooter] = (
|
318
333
|
EmbedFooter(data["footer"]) if data.get("footer") else None
|
@@ -342,7 +357,7 @@ class Embed:
|
|
342
357
|
if self.timestamp:
|
343
358
|
payload["timestamp"] = self.timestamp
|
344
359
|
if self.color is not None:
|
345
|
-
payload["color"] = self.color
|
360
|
+
payload["color"] = self.color.value
|
346
361
|
if self.footer:
|
347
362
|
payload["footer"] = self.footer.to_dict()
|
348
363
|
if self.image:
|
@@ -528,6 +543,12 @@ class Member(User): # Member inherits from User
|
|
528
543
|
def __repr__(self) -> str:
|
529
544
|
return f"<Member id='{self.id}' username='{self.username}' nick='{self.nick}'>"
|
530
545
|
|
546
|
+
@property
|
547
|
+
def display_name(self) -> str:
|
548
|
+
"""Return the nickname if set, otherwise the username."""
|
549
|
+
|
550
|
+
return self.nick or self.username
|
551
|
+
|
531
552
|
async def kick(self, *, reason: Optional[str] = None) -> None:
|
532
553
|
if not self.guild_id or not self._client:
|
533
554
|
raise DisagreementException("Member.kick requires guild_id and client")
|
@@ -1065,6 +1086,19 @@ class TextChannel(Channel):
|
|
1065
1086
|
)
|
1066
1087
|
self.last_pin_timestamp: Optional[str] = data.get("last_pin_timestamp")
|
1067
1088
|
|
1089
|
+
def history(
|
1090
|
+
self,
|
1091
|
+
*,
|
1092
|
+
limit: Optional[int] = None,
|
1093
|
+
before: Optional[str] = None,
|
1094
|
+
after: Optional[str] = None,
|
1095
|
+
) -> AsyncIterator["Message"]:
|
1096
|
+
"""Return an async iterator over this channel's messages."""
|
1097
|
+
|
1098
|
+
from .utils import message_pager
|
1099
|
+
|
1100
|
+
return message_pager(self, limit=limit, before=before, after=after)
|
1101
|
+
|
1068
1102
|
async def send(
|
1069
1103
|
self,
|
1070
1104
|
content: Optional[str] = None,
|
@@ -1224,6 +1258,36 @@ class DMChannel(Channel):
|
|
1224
1258
|
components=components,
|
1225
1259
|
)
|
1226
1260
|
|
1261
|
+
async def history(
|
1262
|
+
self,
|
1263
|
+
*,
|
1264
|
+
limit: Optional[int] = 100,
|
1265
|
+
before: "Snowflake | None" = None,
|
1266
|
+
):
|
1267
|
+
"""An async iterator over messages in this DM."""
|
1268
|
+
|
1269
|
+
params: Dict[str, Union[int, str]] = {}
|
1270
|
+
if before is not None:
|
1271
|
+
params["before"] = before
|
1272
|
+
|
1273
|
+
fetched = 0
|
1274
|
+
while True:
|
1275
|
+
to_fetch = 100 if limit is None else min(100, limit - fetched)
|
1276
|
+
if to_fetch <= 0:
|
1277
|
+
break
|
1278
|
+
params["limit"] = to_fetch
|
1279
|
+
messages = await self._client._http.request(
|
1280
|
+
"GET", f"/channels/{self.id}/messages", params=params.copy()
|
1281
|
+
)
|
1282
|
+
if not messages:
|
1283
|
+
break
|
1284
|
+
params["before"] = messages[-1]["id"]
|
1285
|
+
for msg in messages:
|
1286
|
+
yield Message(msg, self._client)
|
1287
|
+
fetched += 1
|
1288
|
+
if limit is not None and fetched >= limit:
|
1289
|
+
return
|
1290
|
+
|
1227
1291
|
def __repr__(self) -> str:
|
1228
1292
|
recipient_repr = self.recipient.username if self.recipient else "Unknown"
|
1229
1293
|
return f"<DMChannel id='{self.id}' recipient='{recipient_repr}'>"
|
@@ -1317,6 +1381,57 @@ class Webhook:
|
|
1317
1381
|
|
1318
1382
|
return cls({"id": webhook_id, "token": token, "url": url})
|
1319
1383
|
|
1384
|
+
async def send(
|
1385
|
+
self,
|
1386
|
+
content: Optional[str] = None,
|
1387
|
+
*,
|
1388
|
+
username: Optional[str] = None,
|
1389
|
+
avatar_url: Optional[str] = None,
|
1390
|
+
tts: bool = False,
|
1391
|
+
embed: Optional["Embed"] = None,
|
1392
|
+
embeds: Optional[List["Embed"]] = None,
|
1393
|
+
components: Optional[List["ActionRow"]] = None,
|
1394
|
+
allowed_mentions: Optional[Dict[str, Any]] = None,
|
1395
|
+
attachments: Optional[List[Any]] = None,
|
1396
|
+
files: Optional[List[Any]] = None,
|
1397
|
+
flags: Optional[int] = None,
|
1398
|
+
) -> "Message":
|
1399
|
+
"""Send a message using this webhook."""
|
1400
|
+
|
1401
|
+
if not self._client:
|
1402
|
+
raise DisagreementException("Webhook is not bound to a Client")
|
1403
|
+
assert self.token is not None, "Webhook token missing"
|
1404
|
+
|
1405
|
+
if embed and embeds:
|
1406
|
+
raise ValueError("Cannot provide both embed and embeds.")
|
1407
|
+
|
1408
|
+
final_embeds_payload: Optional[List[Dict[str, Any]]] = None
|
1409
|
+
if embed:
|
1410
|
+
final_embeds_payload = [embed.to_dict()]
|
1411
|
+
elif embeds:
|
1412
|
+
final_embeds_payload = [e.to_dict() for e in embeds]
|
1413
|
+
|
1414
|
+
components_payload: Optional[List[Dict[str, Any]]] = None
|
1415
|
+
if components:
|
1416
|
+
components_payload = [c.to_dict() for c in components]
|
1417
|
+
|
1418
|
+
message_data = await self._client._http.execute_webhook(
|
1419
|
+
self.id,
|
1420
|
+
self.token,
|
1421
|
+
content=content,
|
1422
|
+
tts=tts,
|
1423
|
+
embeds=final_embeds_payload,
|
1424
|
+
components=components_payload,
|
1425
|
+
allowed_mentions=allowed_mentions,
|
1426
|
+
attachments=attachments,
|
1427
|
+
files=files,
|
1428
|
+
flags=flags,
|
1429
|
+
username=username,
|
1430
|
+
avatar_url=avatar_url,
|
1431
|
+
)
|
1432
|
+
|
1433
|
+
return self._client.parse_message(message_data)
|
1434
|
+
|
1320
1435
|
|
1321
1436
|
# --- Message Components ---
|
1322
1437
|
|
@@ -1708,13 +1823,13 @@ class Container(Component):
|
|
1708
1823
|
def __init__(
|
1709
1824
|
self,
|
1710
1825
|
components: List[Component],
|
1711
|
-
accent_color:
|
1826
|
+
accent_color: Color | int | str | None = None,
|
1712
1827
|
spoiler: bool = False,
|
1713
1828
|
id: Optional[int] = None,
|
1714
1829
|
):
|
1715
1830
|
super().__init__(ComponentType.CONTAINER)
|
1716
1831
|
self.components = components
|
1717
|
-
self.accent_color = accent_color
|
1832
|
+
self.accent_color = Color.parse(accent_color)
|
1718
1833
|
self.spoiler = spoiler
|
1719
1834
|
self.id = id
|
1720
1835
|
|
@@ -1722,7 +1837,7 @@ class Container(Component):
|
|
1722
1837
|
payload = super().to_dict()
|
1723
1838
|
payload["components"] = [c.to_dict() for c in self.components]
|
1724
1839
|
if self.accent_color:
|
1725
|
-
payload["accent_color"] = self.accent_color
|
1840
|
+
payload["accent_color"] = self.accent_color.value
|
1726
1841
|
if self.spoiler:
|
1727
1842
|
payload["spoiler"] = self.spoiler
|
1728
1843
|
if self.id is not None:
|
disagreement/py.typed
ADDED
File without changes
|
disagreement/ui/modal.py
CHANGED
disagreement/utils.py
CHANGED
@@ -3,8 +3,71 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
from datetime import datetime, timezone
|
6
|
+
from typing import Any, AsyncIterator, Dict, Optional, TYPE_CHECKING
|
7
|
+
|
8
|
+
if TYPE_CHECKING: # pragma: no cover - for type hinting only
|
9
|
+
from .models import Message, TextChannel
|
6
10
|
|
7
11
|
|
8
12
|
def utcnow() -> datetime:
|
9
13
|
"""Return the current timezone-aware UTC time."""
|
10
14
|
return datetime.now(timezone.utc)
|
15
|
+
|
16
|
+
|
17
|
+
async def message_pager(
|
18
|
+
channel: "TextChannel",
|
19
|
+
*,
|
20
|
+
limit: Optional[int] = None,
|
21
|
+
before: Optional[str] = None,
|
22
|
+
after: Optional[str] = None,
|
23
|
+
) -> AsyncIterator["Message"]:
|
24
|
+
"""Asynchronously paginate a channel's messages.
|
25
|
+
|
26
|
+
Parameters
|
27
|
+
----------
|
28
|
+
channel:
|
29
|
+
The :class:`TextChannel` to fetch messages from.
|
30
|
+
limit:
|
31
|
+
The maximum number of messages to yield. ``None`` fetches until no
|
32
|
+
more messages are returned.
|
33
|
+
before:
|
34
|
+
Fetch messages with IDs less than this snowflake.
|
35
|
+
after:
|
36
|
+
Fetch messages with IDs greater than this snowflake.
|
37
|
+
|
38
|
+
Yields
|
39
|
+
------
|
40
|
+
Message
|
41
|
+
Messages in the channel, oldest first.
|
42
|
+
"""
|
43
|
+
|
44
|
+
remaining = limit
|
45
|
+
last_id = before
|
46
|
+
while remaining is None or remaining > 0:
|
47
|
+
fetch_limit = 100
|
48
|
+
if remaining is not None:
|
49
|
+
fetch_limit = min(fetch_limit, remaining)
|
50
|
+
|
51
|
+
params: Dict[str, Any] = {"limit": fetch_limit}
|
52
|
+
if last_id is not None:
|
53
|
+
params["before"] = last_id
|
54
|
+
if after is not None:
|
55
|
+
params["after"] = after
|
56
|
+
|
57
|
+
data = await channel._client._http.request( # type: ignore[attr-defined]
|
58
|
+
"GET",
|
59
|
+
f"/channels/{channel.id}/messages",
|
60
|
+
params=params,
|
61
|
+
)
|
62
|
+
|
63
|
+
if not data:
|
64
|
+
break
|
65
|
+
|
66
|
+
for raw in data:
|
67
|
+
msg = channel._client.parse_message(raw) # type: ignore[attr-defined]
|
68
|
+
yield msg
|
69
|
+
last_id = msg.id
|
70
|
+
if remaining is not None:
|
71
|
+
remaining -= 1
|
72
|
+
if remaining == 0:
|
73
|
+
return
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: disagreement
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.0rc3
|
4
4
|
Summary: A Python library for the Discord API.
|
5
5
|
Author-email: Slipstream <me@slipstreamm.dev>
|
6
6
|
License: BSD 3-Clause
|
@@ -12,22 +12,23 @@ Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: License :: OSI Approved :: BSD License
|
13
13
|
Classifier: Operating System :: OS Independent
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
15
16
|
Classifier: Programming Language :: Python :: 3.11
|
16
17
|
Classifier: Programming Language :: Python :: 3.12
|
17
18
|
Classifier: Programming Language :: Python :: 3.13
|
18
19
|
Classifier: Topic :: Software Development :: Libraries
|
19
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
20
21
|
Classifier: Topic :: Internet
|
21
|
-
Requires-Python: >=3.
|
22
|
+
Requires-Python: >=3.10
|
22
23
|
Description-Content-Type: text/markdown
|
23
24
|
License-File: LICENSE
|
24
25
|
Requires-Dist: aiohttp<4.0.0,>=3.9.0
|
25
26
|
Provides-Extra: test
|
26
27
|
Requires-Dist: pytest>=8.0.0; extra == "test"
|
27
28
|
Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
|
28
|
-
Requires-Dist: hypothesis>=6.
|
29
|
+
Requires-Dist: hypothesis>=6.132.0; extra == "test"
|
29
30
|
Provides-Extra: dev
|
30
|
-
Requires-Dist: dotenv>=0.0
|
31
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
|
31
32
|
Dynamic: license-file
|
32
33
|
|
33
34
|
# Disagreement
|
@@ -53,7 +54,7 @@ pip install disagreement
|
|
53
54
|
pip install -e .
|
54
55
|
```
|
55
56
|
|
56
|
-
Requires Python 3.
|
57
|
+
Requires Python 3.10 or newer.
|
57
58
|
|
58
59
|
## Basic Usage
|
59
60
|
|