disagreement 0.3.0b1__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 +25 -5
  3. disagreement/cache.py +12 -3
  4. disagreement/caching.py +15 -14
  5. disagreement/client.py +86 -52
  6. disagreement/enums.py +10 -3
  7. disagreement/error_handler.py +5 -1
  8. disagreement/errors.py +1341 -3
  9. disagreement/event_dispatcher.py +1 -3
  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 +0 -2
  19. disagreement/ext/commands/cog.py +0 -2
  20. disagreement/ext/commands/converters.py +16 -5
  21. disagreement/ext/commands/core.py +52 -14
  22. disagreement/ext/commands/decorators.py +3 -7
  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 +27 -25
  27. disagreement/http.py +264 -22
  28. disagreement/interactions.py +0 -2
  29. disagreement/models.py +199 -105
  30. disagreement/shard_manager.py +0 -2
  31. disagreement/ui/view.py +2 -2
  32. disagreement/voice_client.py +20 -1
  33. {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/METADATA +32 -6
  34. disagreement-0.4.0.dist-info/RECORD +55 -0
  35. disagreement-0.3.0b1.dist-info/RECORD +0 -55
  36. {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/WHEEL +0 -0
  37. {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/licenses/LICENSE +0 -0
  38. {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/top_level.txt +0 -0
disagreement/models.py CHANGED
@@ -1,11 +1,10 @@
1
- # disagreement/models.py
2
-
3
1
  """
4
2
  Data models for Discord objects.
5
3
  """
6
4
 
7
5
  import asyncio
8
6
  import json
7
+ import re
9
8
  from dataclasses import dataclass
10
9
  from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union, cast
11
10
 
@@ -24,6 +23,7 @@ from .enums import ( # These enums will need to be defined in disagreement/enum
24
23
  PremiumTier,
25
24
  GuildFeature,
26
25
  ChannelType,
26
+ AutoArchiveDuration,
27
27
  ComponentType,
28
28
  ButtonStyle, # Added for Button
29
29
  GuildScheduledEventPrivacyLevel,
@@ -39,6 +39,7 @@ if TYPE_CHECKING:
39
39
  from .enums import OverwriteType # For PermissionOverwrite model
40
40
  from .ui.view import View
41
41
  from .interactions import Snowflake
42
+ from .typing import Typing
42
43
 
43
44
  # Forward reference Message if it were used in type hints before its definition
44
45
  # from .models import Message # Not needed as Message is defined before its use in TextChannel.send etc.
@@ -114,31 +115,39 @@ class Message:
114
115
  # self.mention_roles: List[str] = data.get("mention_roles", [])
115
116
  # self.mention_everyone: bool = data.get("mention_everyone", False)
116
117
 
118
+ @property
119
+ def clean_content(self) -> str:
120
+ """Returns message content without user, role, or channel mentions."""
121
+
122
+ pattern = re.compile(r"<@!?\d+>|<#\d+>|<@&\d+>")
123
+ cleaned = pattern.sub("", self.content)
124
+ return " ".join(cleaned.split())
125
+
117
126
  async def pin(self) -> None:
118
- """|coro|
127
+ """|coro|
119
128
 
120
- Pins this message to its channel.
129
+ Pins this message to its channel.
121
130
 
122
- Raises
123
- ------
124
- HTTPException
125
- Pinning the message failed.
126
- """
127
- await self._client._http.pin_message(self.channel_id, self.id)
128
- self.pinned = True
131
+ Raises
132
+ ------
133
+ HTTPException
134
+ Pinning the message failed.
135
+ """
136
+ await self._client._http.pin_message(self.channel_id, self.id)
137
+ self.pinned = True
129
138
 
130
139
  async def unpin(self) -> None:
131
- """|coro|
140
+ """|coro|
132
141
 
133
- Unpins this message from its channel.
142
+ Unpins this message from its channel.
134
143
 
135
- Raises
136
- ------
137
- HTTPException
138
- Unpinning the message failed.
139
- """
140
- await self._client._http.unpin_message(self.channel_id, self.id)
141
- self.pinned = False
144
+ Raises
145
+ ------
146
+ HTTPException
147
+ Unpinning the message failed.
148
+ """
149
+ await self._client._http.unpin_message(self.channel_id, self.id)
150
+ self.pinned = False
142
151
 
143
152
  async def reply(
144
153
  self,
@@ -241,16 +250,16 @@ class Message:
241
250
  await self._client.add_reaction(self.channel_id, self.id, emoji)
242
251
 
243
252
  async def remove_reaction(self, emoji: str, member: Optional[User] = None) -> None:
244
- """|coro|
245
- Removes a reaction from this message.
246
- If no ``member`` is provided, removes the bot's own reaction.
247
- """
248
- if member:
249
- await self._client._http.delete_user_reaction(
250
- self.channel_id, self.id, emoji, member.id
251
- )
252
- else:
253
- await self._client.remove_reaction(self.channel_id, self.id, emoji)
253
+ """|coro|
254
+ Removes a reaction from this message.
255
+ If no ``member`` is provided, removes the bot's own reaction.
256
+ """
257
+ if member:
258
+ await self._client._http.delete_user_reaction(
259
+ self.channel_id, self.id, emoji, member.id
260
+ )
261
+ else:
262
+ await self._client.remove_reaction(self.channel_id, self.id, emoji)
254
263
 
255
264
  async def clear_reactions(self) -> None:
256
265
  """|coro| Remove all reactions from this message."""
@@ -280,7 +289,7 @@ class Message:
280
289
  self,
281
290
  name: str,
282
291
  *,
283
- auto_archive_duration: Optional[int] = None,
292
+ auto_archive_duration: Optional[AutoArchiveDuration] = None,
284
293
  rate_limit_per_user: Optional[int] = None,
285
294
  reason: Optional[str] = None,
286
295
  ) -> "Thread":
@@ -292,9 +301,9 @@ class Message:
292
301
  ----------
293
302
  name: str
294
303
  The name of the thread.
295
- auto_archive_duration: Optional[int]
296
- The duration in minutes to automatically archive the thread after recent activity.
297
- Can be one of 60, 1440, 4320, 10080.
304
+ auto_archive_duration: Optional[AutoArchiveDuration]
305
+ How long before the thread is automatically archived after recent activity.
306
+ See :class:`AutoArchiveDuration` for allowed values.
298
307
  rate_limit_per_user: Optional[int]
299
308
  The number of seconds a user has to wait before sending another message.
300
309
  reason: Optional[str]
@@ -307,7 +316,7 @@ class Message:
307
316
  """
308
317
  payload: Dict[str, Any] = {"name": name}
309
318
  if auto_archive_duration is not None:
310
- payload["auto_archive_duration"] = auto_archive_duration
319
+ payload["auto_archive_duration"] = int(auto_archive_duration)
311
320
  if rate_limit_per_user is not None:
312
321
  payload["rate_limit_per_user"] = rate_limit_per_user
313
322
 
@@ -530,8 +539,42 @@ class Embed:
530
539
  payload["fields"] = [f.to_dict() for f in self.fields]
531
540
  return payload
532
541
 
533
- # Convenience methods for building embeds can be added here
534
- # e.g., set_author, add_field, set_footer, set_image, etc.
542
+ # Convenience methods mirroring ``discord.py``'s ``Embed`` API
543
+
544
+ def set_author(
545
+ self, *, name: str, url: Optional[str] = None, icon_url: Optional[str] = None
546
+ ) -> "Embed":
547
+ """Set the embed author and return ``self`` for chaining."""
548
+
549
+ data: Dict[str, Any] = {"name": name}
550
+ if url:
551
+ data["url"] = url
552
+ if icon_url:
553
+ data["icon_url"] = icon_url
554
+ self.author = EmbedAuthor(data)
555
+ return self
556
+
557
+ def add_field(self, *, name: str, value: str, inline: bool = False) -> "Embed":
558
+ """Add a field to the embed."""
559
+
560
+ field = EmbedField({"name": name, "value": value, "inline": inline})
561
+ self.fields.append(field)
562
+ return self
563
+
564
+ def set_footer(self, *, text: str, icon_url: Optional[str] = None) -> "Embed":
565
+ """Set the embed footer."""
566
+
567
+ data: Dict[str, Any] = {"text": text}
568
+ if icon_url:
569
+ data["icon_url"] = icon_url
570
+ self.footer = EmbedFooter(data)
571
+ return self
572
+
573
+ def set_image(self, url: str) -> "Embed":
574
+ """Set the embed image."""
575
+
576
+ self.image = EmbedImage({"url": url})
577
+ return self
535
578
 
536
579
 
537
580
  class Attachment:
@@ -669,7 +712,7 @@ class Member(User): # Member inherits from User
669
712
  # We'd need to construct a partial user from top-level member fields if 'user' is missing.
670
713
  # For now, assume 'user' object is present for full Member hydration.
671
714
  # If 'user' is missing, the User part might be incomplete.
672
- pass # User fields will be missing or default if 'user' not in data.
715
+ pass
673
716
 
674
717
  super().__init__(
675
718
  user_data if user_data else data
@@ -1088,7 +1131,9 @@ class Guild:
1088
1131
 
1089
1132
  # Internal caches, populated by events or specific fetches
1090
1133
  self._channels: ChannelCache = ChannelCache()
1091
- self._members: MemberCache = MemberCache(getattr(client_instance, "member_cache_flags", MemberCacheFlags()))
1134
+ self._members: MemberCache = MemberCache(
1135
+ getattr(client_instance, "member_cache_flags", MemberCacheFlags())
1136
+ )
1092
1137
  self._threads: Dict[str, "Thread"] = {}
1093
1138
 
1094
1139
  def get_channel(self, channel_id: str) -> Optional["Channel"]:
@@ -1128,6 +1173,16 @@ class Guild:
1128
1173
  def __repr__(self) -> str:
1129
1174
  return f"<Guild id='{self.id}' name='{self.name}'>"
1130
1175
 
1176
+ async def fetch_widget(self) -> Dict[str, Any]:
1177
+ """|coro| Fetch this guild's widget settings."""
1178
+
1179
+ return await self._client.fetch_widget(self.id)
1180
+
1181
+ async def edit_widget(self, payload: Dict[str, Any]) -> Dict[str, Any]:
1182
+ """|coro| Edit this guild's widget settings."""
1183
+
1184
+ return await self._client.edit_widget(self.id, payload)
1185
+
1131
1186
  async def fetch_members(self, *, limit: Optional[int] = None) -> List["Member"]:
1132
1187
  """|coro|
1133
1188
 
@@ -1278,7 +1333,45 @@ class Channel:
1278
1333
  return base
1279
1334
 
1280
1335
 
1281
- class TextChannel(Channel):
1336
+ class Messageable:
1337
+ """Mixin for channels that can send messages and show typing."""
1338
+
1339
+ _client: "Client"
1340
+ id: str
1341
+
1342
+ async def send(
1343
+ self,
1344
+ content: Optional[str] = None,
1345
+ *,
1346
+ embed: Optional["Embed"] = None,
1347
+ embeds: Optional[List["Embed"]] = None,
1348
+ components: Optional[List["ActionRow"]] = None,
1349
+ ) -> "Message":
1350
+ if not hasattr(self._client, "send_message"):
1351
+ raise NotImplementedError(
1352
+ "Client.send_message is required for Messageable.send"
1353
+ )
1354
+
1355
+ return await self._client.send_message(
1356
+ channel_id=self.id,
1357
+ content=content,
1358
+ embed=embed,
1359
+ embeds=embeds,
1360
+ components=components,
1361
+ )
1362
+
1363
+ async def trigger_typing(self) -> None:
1364
+ await self._client._http.trigger_typing(self.id)
1365
+
1366
+ def typing(self) -> "Typing":
1367
+ if not hasattr(self._client, "typing"):
1368
+ raise NotImplementedError(
1369
+ "Client.typing is required for Messageable.typing"
1370
+ )
1371
+ return self._client.typing(self.id)
1372
+
1373
+
1374
+ class TextChannel(Channel, Messageable):
1282
1375
  """Represents a guild text channel or announcement channel."""
1283
1376
 
1284
1377
  def __init__(self, data: Dict[str, Any], client_instance: "Client"):
@@ -1304,27 +1397,6 @@ class TextChannel(Channel):
1304
1397
 
1305
1398
  return message_pager(self, limit=limit, before=before, after=after)
1306
1399
 
1307
- async def send(
1308
- self,
1309
- content: Optional[str] = None,
1310
- *,
1311
- embed: Optional[Embed] = None,
1312
- embeds: Optional[List[Embed]] = None,
1313
- components: Optional[List["ActionRow"]] = None, # Added components
1314
- ) -> "Message": # Forward reference Message
1315
- if not hasattr(self._client, "send_message"):
1316
- raise NotImplementedError(
1317
- "Client.send_message is required for TextChannel.send"
1318
- )
1319
-
1320
- return await self._client.send_message(
1321
- channel_id=self.id,
1322
- content=content,
1323
- embed=embed,
1324
- embeds=embeds,
1325
- components=components,
1326
- )
1327
-
1328
1400
  async def purge(
1329
1401
  self, limit: int, *, before: "Snowflake | None" = None
1330
1402
  ) -> List["Snowflake"]:
@@ -1347,41 +1419,41 @@ class TextChannel(Channel):
1347
1419
  return ids
1348
1420
 
1349
1421
  def get_partial_message(self, id: int) -> "PartialMessage":
1350
- """Returns a :class:`PartialMessage` for the given ID.
1422
+ """Returns a :class:`PartialMessage` for the given ID.
1351
1423
 
1352
- This allows performing actions on a message without fetching it first.
1424
+ This allows performing actions on a message without fetching it first.
1353
1425
 
1354
- Parameters
1355
- ----------
1356
- id: int
1357
- The ID of the message to get a partial instance of.
1426
+ Parameters
1427
+ ----------
1428
+ id: int
1429
+ The ID of the message to get a partial instance of.
1358
1430
 
1359
- Returns
1360
- -------
1361
- PartialMessage
1362
- The partial message instance.
1363
- """
1364
- return PartialMessage(id=str(id), channel=self)
1431
+ Returns
1432
+ -------
1433
+ PartialMessage
1434
+ The partial message instance.
1435
+ """
1436
+ return PartialMessage(id=str(id), channel=self)
1365
1437
 
1366
1438
  def __repr__(self) -> str:
1367
1439
  return f"<TextChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
1368
1440
 
1369
1441
  async def pins(self) -> List["Message"]:
1370
1442
  """|coro|
1371
-
1443
+
1372
1444
  Fetches all pinned messages in this channel.
1373
-
1445
+
1374
1446
  Returns
1375
1447
  -------
1376
1448
  List[Message]
1377
1449
  The pinned messages.
1378
-
1450
+
1379
1451
  Raises
1380
1452
  ------
1381
1453
  HTTPException
1382
1454
  Fetching the pinned messages failed.
1383
1455
  """
1384
-
1456
+
1385
1457
  messages_data = await self._client._http.get_pinned_messages(self.id)
1386
1458
  return [self._client.parse_message(m) for m in messages_data]
1387
1459
 
@@ -1390,7 +1462,7 @@ class TextChannel(Channel):
1390
1462
  name: str,
1391
1463
  *,
1392
1464
  type: ChannelType = ChannelType.PUBLIC_THREAD,
1393
- auto_archive_duration: Optional[int] = None,
1465
+ auto_archive_duration: Optional[AutoArchiveDuration] = None,
1394
1466
  invitable: Optional[bool] = None,
1395
1467
  rate_limit_per_user: Optional[int] = None,
1396
1468
  reason: Optional[str] = None,
@@ -1406,8 +1478,8 @@ class TextChannel(Channel):
1406
1478
  type: ChannelType
1407
1479
  The type of thread to create. Defaults to PUBLIC_THREAD.
1408
1480
  Can be PUBLIC_THREAD, PRIVATE_THREAD, or ANNOUNCEMENT_THREAD.
1409
- auto_archive_duration: Optional[int]
1410
- The duration in minutes to automatically archive the thread after recent activity.
1481
+ auto_archive_duration: Optional[AutoArchiveDuration]
1482
+ How long before the thread is automatically archived after recent activity.
1411
1483
  invitable: Optional[bool]
1412
1484
  Whether non-moderators can invite other non-moderators to a private thread.
1413
1485
  Only applicable to private threads.
@@ -1426,7 +1498,7 @@ class TextChannel(Channel):
1426
1498
  "type": type.value,
1427
1499
  }
1428
1500
  if auto_archive_duration is not None:
1429
- payload["auto_archive_duration"] = auto_archive_duration
1501
+ payload["auto_archive_duration"] = int(auto_archive_duration)
1430
1502
  if invitable is not None and type == ChannelType.PRIVATE_THREAD:
1431
1503
  payload["invitable"] = invitable
1432
1504
  if rate_limit_per_user is not None:
@@ -1606,7 +1678,9 @@ class Thread(TextChannel): # Threads are a specialized TextChannel
1606
1678
  """
1607
1679
  await self._client._http.leave_thread(self.id)
1608
1680
 
1609
- async def archive(self, locked: bool = False, *, reason: Optional[str] = None) -> "Thread":
1681
+ async def archive(
1682
+ self, locked: bool = False, *, reason: Optional[str] = None
1683
+ ) -> "Thread":
1610
1684
  """|coro|
1611
1685
 
1612
1686
  Archives this thread.
@@ -1631,7 +1705,7 @@ class Thread(TextChannel): # Threads are a specialized TextChannel
1631
1705
  return cast("Thread", self._client.parse_channel(data))
1632
1706
 
1633
1707
 
1634
- class DMChannel(Channel):
1708
+ class DMChannel(Channel, Messageable):
1635
1709
  """Represents a Direct Message channel."""
1636
1710
 
1637
1711
  def __init__(self, data: Dict[str, Any], client_instance: "Client"):
@@ -1645,27 +1719,6 @@ class DMChannel(Channel):
1645
1719
  def recipient(self) -> Optional[User]:
1646
1720
  return self.recipients[0] if self.recipients else None
1647
1721
 
1648
- async def send(
1649
- self,
1650
- content: Optional[str] = None,
1651
- *,
1652
- embed: Optional[Embed] = None,
1653
- embeds: Optional[List[Embed]] = None,
1654
- components: Optional[List["ActionRow"]] = None, # Added components
1655
- ) -> "Message":
1656
- if not hasattr(self._client, "send_message"):
1657
- raise NotImplementedError(
1658
- "Client.send_message is required for DMChannel.send"
1659
- )
1660
-
1661
- return await self._client.send_message(
1662
- channel_id=self.id,
1663
- content=content,
1664
- embed=embed,
1665
- embeds=embeds,
1666
- components=components,
1667
- )
1668
-
1669
1722
  async def history(
1670
1723
  self,
1671
1724
  *,
@@ -2356,6 +2409,37 @@ class ThreadMember:
2356
2409
  return f"<ThreadMember user_id='{self.user_id}' thread_id='{self.id}'>"
2357
2410
 
2358
2411
 
2412
+ class Activity:
2413
+ """Represents a user's presence activity."""
2414
+
2415
+ def __init__(self, name: str, type: int) -> None:
2416
+ self.name = name
2417
+ self.type = type
2418
+
2419
+ def to_dict(self) -> Dict[str, Any]:
2420
+ return {"name": self.name, "type": self.type}
2421
+
2422
+
2423
+ class Game(Activity):
2424
+ """Represents a playing activity."""
2425
+
2426
+ def __init__(self, name: str) -> None:
2427
+ super().__init__(name, 0)
2428
+
2429
+
2430
+ class Streaming(Activity):
2431
+ """Represents a streaming activity."""
2432
+
2433
+ def __init__(self, name: str, url: str) -> None:
2434
+ super().__init__(name, 1)
2435
+ self.url = url
2436
+
2437
+ def to_dict(self) -> Dict[str, Any]:
2438
+ payload = super().to_dict()
2439
+ payload["url"] = self.url
2440
+ return payload
2441
+
2442
+
2359
2443
  class PresenceUpdate:
2360
2444
  """Represents a PRESENCE_UPDATE event."""
2361
2445
 
@@ -2366,7 +2450,17 @@ class PresenceUpdate:
2366
2450
  self.user = User(data["user"])
2367
2451
  self.guild_id: Optional[str] = data.get("guild_id")
2368
2452
  self.status: Optional[str] = data.get("status")
2369
- self.activities: List[Dict[str, Any]] = data.get("activities", [])
2453
+ self.activities: List[Activity] = []
2454
+ for activity in data.get("activities", []):
2455
+ act_type = activity.get("type", 0)
2456
+ name = activity.get("name", "")
2457
+ if act_type == 0:
2458
+ obj = Game(name)
2459
+ elif act_type == 1:
2460
+ obj = Streaming(name, activity.get("url", ""))
2461
+ else:
2462
+ obj = Activity(name, act_type)
2463
+ self.activities.append(obj)
2370
2464
  self.client_status: Dict[str, Any] = data.get("client_status", {})
2371
2465
 
2372
2466
  def __repr__(self) -> str:
@@ -1,5 +1,3 @@
1
- # disagreement/shard_manager.py
2
-
3
1
  """Sharding utilities for managing multiple gateway connections."""
4
2
 
5
3
  from __future__ import annotations
disagreement/ui/view.py CHANGED
@@ -72,7 +72,7 @@ class View:
72
72
  rows: List[ActionRow] = []
73
73
 
74
74
  for item in self.children:
75
- rows.append(ActionRow(components=[item]))
75
+ rows.append(ActionRow(components=[item]))
76
76
 
77
77
  return rows
78
78
 
@@ -147,7 +147,7 @@ class View:
147
147
 
148
148
  async def on_timeout(self):
149
149
  """Called when the view times out."""
150
- pass # User can override this
150
+ pass
151
151
 
152
152
  async def _start(self, client: Client):
153
153
  """Starts the view's internal listener."""
@@ -1,4 +1,3 @@
1
- # disagreement/voice_client.py
2
1
  """Voice gateway and UDP audio client."""
3
2
 
4
3
  from __future__ import annotations
@@ -7,9 +6,26 @@ import asyncio
7
6
  import contextlib
8
7
  import socket
9
8
  import threading
9
+ from array import array
10
+
11
+
12
+ def _apply_volume(data: bytes, volume: float) -> bytes:
13
+ samples = array("h")
14
+ samples.frombytes(data)
15
+ for i, sample in enumerate(samples):
16
+ scaled = int(sample * volume)
17
+ if scaled > 32767:
18
+ scaled = 32767
19
+ elif scaled < -32768:
20
+ scaled = -32768
21
+ samples[i] = scaled
22
+ return samples.tobytes()
23
+
24
+
10
25
  from typing import TYPE_CHECKING, Optional, Sequence
11
26
 
12
27
  import aiohttp
28
+
13
29
  # The following import is correct, but may be flagged by Pylance if the virtual
14
30
  # environment is not configured correctly.
15
31
  from nacl.secret import SecretBox
@@ -180,6 +196,9 @@ class VoiceClient:
180
196
  data = await self._current_source.read()
181
197
  if not data:
182
198
  break
199
+ volume = getattr(self._current_source, "volume", 1.0)
200
+ if volume != 1.0:
201
+ data = _apply_volume(data, volume)
183
202
  await self.send_audio_frame(data)
184
203
  finally:
185
204
  await self._current_source.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.3.0b1
3
+ Version: 0.4.0
4
4
  Summary: A Python library for the Discord API.
5
5
  Author-email: Slipstream <me@slipstreamm.dev>
6
6
  License: BSD 3-Clause
@@ -38,6 +38,9 @@ A Python library for interacting with the Discord API, with a focus on bot devel
38
38
 
39
39
  ## Features
40
40
 
41
+ - Internationalization helpers
42
+ - Hybrid context for commands
43
+ - Built-in rate limiting
41
44
  - Asynchronous design using `aiohttp`
42
45
  - Gateway and HTTP API clients
43
46
  - Slash command framework
@@ -57,6 +60,13 @@ pip install -e .
57
60
 
58
61
  Requires Python 3.10 or newer.
59
62
 
63
+ To run the example scripts, you'll need the `python-dotenv` package to load
64
+ environment variables. Install the development extras with:
65
+
66
+ ```bash
67
+ pip install "disagreement[dev]"
68
+ ```
69
+
60
70
  ## Basic Usage
61
71
 
62
72
  ```python
@@ -65,6 +75,8 @@ import os
65
75
 
66
76
  import disagreement
67
77
  from disagreement.ext import commands
78
+ from dotenv import load_dotenv
79
+ load_dotenv()
68
80
 
69
81
 
70
82
  class Basics(commands.Cog):
@@ -73,18 +85,17 @@ class Basics(commands.Cog):
73
85
 
74
86
  @commands.command()
75
87
  async def ping(self, ctx: commands.CommandContext) -> None:
76
- await ctx.reply("Pong!")
88
+ await ctx.reply(f"Pong! Gateway Latency: {self.client.latency_ms} ms.")
77
89
 
78
90
 
79
91
  token = os.getenv("DISCORD_BOT_TOKEN")
80
92
  if not token:
81
93
  raise RuntimeError("DISCORD_BOT_TOKEN environment variable not set")
82
94
 
83
- client = disagreement.Client(token=token, command_prefix="!")
84
- client.add_cog(Basics(client))
85
-
86
-
95
+ intents = disagreement.GatewayIntent.default() | disagreement.GatewayIntent.MESSAGE_CONTENT
96
+ client = disagreement.Client(token=token, command_prefix="!", intents=intents, mention_replies=True)
87
97
  async def main() -> None:
98
+ client.add_cog(Basics(client))
88
99
  await client.run()
89
100
 
90
101
 
@@ -136,6 +147,20 @@ These options are forwarded to ``HTTPClient`` when it creates the underlying
136
147
  ``aiohttp.ClientSession``. You can specify a custom ``connector`` or any other
137
148
  session parameter supported by ``aiohttp``.
138
149
 
150
+ ### Default Allowed Mentions
151
+
152
+ Specify default mention behaviour for all outgoing messages when constructing the client:
153
+
154
+ ```python
155
+ client = disagreement.Client(
156
+ token=token,
157
+ allowed_mentions={"parse": [], "replied_user": False},
158
+ )
159
+ ```
160
+
161
+ This dictionary is used whenever ``send_message`` is called without an explicit
162
+ ``allowed_mentions`` argument.
163
+
139
164
  ### Defining Subcommands with `AppCommandGroup`
140
165
 
141
166
  ```python
@@ -154,6 +179,7 @@ async def show(ctx: AppCommandContext, key: str):
154
179
  @slash_command(name="set", description="Update a setting.", parent=admin_group)
155
180
  async def set_setting(ctx: AppCommandContext, key: str, value: str):
156
181
  ...
182
+ ```
157
183
  ## Fetching Guilds
158
184
 
159
185
  Use `Client.fetch_guild` to retrieve a guild from the Discord API if it