disagreement 0.4.1__py3-none-any.whl → 0.4.2__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 CHANGED
@@ -12,7 +12,7 @@ __title__ = "disagreement"
12
12
  __author__ = "Slipstream"
13
13
  __license__ = "BSD 3-Clause License"
14
14
  __copyright__ = "Copyright 2025 Slipstream"
15
- __version__ = "0.4.1"
15
+ __version__ = "0.4.2"
16
16
 
17
17
  from .client import Client, AutoShardedClient
18
18
  from .models import Message, User, Reaction, AuditLogEntry
@@ -38,6 +38,35 @@ from .logging_config import setup_logging
38
38
  import logging
39
39
 
40
40
 
41
+ __all__ = [
42
+ "Client",
43
+ "AutoShardedClient",
44
+ "Message",
45
+ "User",
46
+ "Reaction",
47
+ "AuditLogEntry",
48
+ "VoiceClient",
49
+ "AudioSource",
50
+ "FFmpegAudioSource",
51
+ "Typing",
52
+ "DisagreementException",
53
+ "HTTPException",
54
+ "GatewayException",
55
+ "AuthenticationError",
56
+ "Forbidden",
57
+ "NotFound",
58
+ "Color",
59
+ "utcnow",
60
+ "message_pager",
61
+ "GatewayIntent",
62
+ "GatewayOpcode",
63
+ "setup_global_error_handler",
64
+ "HybridContext",
65
+ "tasks",
66
+ "setup_logging",
67
+ ]
68
+
69
+
41
70
  # Configure a default logger if none has been configured yet
42
71
  if not logging.getLogger().hasHandlers():
43
72
  setup_logging(logging.INFO)
disagreement/cache.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
- from typing import TYPE_CHECKING, Dict, Generic, Optional, TypeVar
4
+ from typing import TYPE_CHECKING, Callable, Dict, Generic, Optional, TypeVar
5
5
  from collections import OrderedDict
6
6
 
7
7
  if TYPE_CHECKING:
@@ -40,6 +40,14 @@ class Cache(Generic[T]):
40
40
  self._data.move_to_end(key)
41
41
  return value
42
42
 
43
+ def get_or_fetch(self, key: str, fetch_fn: Callable[[], T]) -> T:
44
+ """Return a cached item or fetch and store it if missing."""
45
+ value = self.get(key)
46
+ if value is None:
47
+ value = fetch_fn()
48
+ self.set(key, value)
49
+ return value
50
+
43
51
  def invalidate(self, key: str) -> None:
44
52
  self._data.pop(key, None)
45
53
 
disagreement/client.py CHANGED
@@ -529,15 +529,29 @@ class Client:
529
529
  print(f"Message: {message.content}")
530
530
  """
531
531
 
532
- def decorator(
533
- coro: Callable[..., Awaitable[None]],
534
- ) -> Callable[..., Awaitable[None]]:
535
- if not asyncio.iscoroutinefunction(coro):
536
- raise TypeError("Event registered must be a coroutine function.")
537
- self._event_dispatcher.register(event_name.upper(), coro)
538
- return coro
539
-
540
- return decorator
532
+ def decorator(
533
+ coro: Callable[..., Awaitable[None]],
534
+ ) -> Callable[..., Awaitable[None]]:
535
+ if not asyncio.iscoroutinefunction(coro):
536
+ raise TypeError("Event registered must be a coroutine function.")
537
+ self._event_dispatcher.register(event_name.upper(), coro)
538
+ return coro
539
+
540
+ return decorator
541
+
542
+ def add_listener(
543
+ self, event_name: str, coro: Callable[..., Awaitable[None]]
544
+ ) -> None:
545
+ """Register ``coro`` to listen for ``event_name``."""
546
+
547
+ self._event_dispatcher.register(event_name, coro)
548
+
549
+ def remove_listener(
550
+ self, event_name: str, coro: Callable[..., Awaitable[None]]
551
+ ) -> None:
552
+ """Remove ``coro`` from ``event_name`` listeners."""
553
+
554
+ self._event_dispatcher.unregister(event_name, coro)
541
555
 
542
556
  async def _process_message_for_commands(self, message: "Message") -> None:
543
557
  """Internal listener to process messages for commands."""
@@ -1311,8 +1325,8 @@ class Client:
1311
1325
 
1312
1326
  return self._messages.get(message_id)
1313
1327
 
1314
- async def fetch_guild(self, guild_id: Snowflake) -> Optional["Guild"]:
1315
- """Fetches a guild by ID from Discord and caches it."""
1328
+ async def fetch_guild(self, guild_id: Snowflake) -> Optional["Guild"]:
1329
+ """Fetches a guild by ID from Discord and caches it."""
1316
1330
 
1317
1331
  if self._closed:
1318
1332
  raise DisagreementException("Client is closed.")
@@ -1326,7 +1340,19 @@ class Client:
1326
1340
  return self.parse_guild(guild_data)
1327
1341
  except DisagreementException as e:
1328
1342
  print(f"Failed to fetch guild {guild_id}: {e}")
1329
- return None
1343
+ return None
1344
+
1345
+ async def fetch_guilds(self) -> List["Guild"]:
1346
+ """Fetch all guilds the current user is in."""
1347
+
1348
+ if self._closed:
1349
+ raise DisagreementException("Client is closed.")
1350
+
1351
+ data = await self._http.get_current_user_guilds()
1352
+ guilds: List["Guild"] = []
1353
+ for guild_data in data:
1354
+ guilds.append(self.parse_guild(guild_data))
1355
+ return guilds
1330
1356
 
1331
1357
  async def fetch_channel(self, channel_id: Snowflake) -> Optional["Channel"]:
1332
1358
  """Fetches a channel from Discord by its ID and updates the cache."""
@@ -60,6 +60,7 @@ class EventDispatcher:
60
60
  "GUILD_BAN_REMOVE": self._parse_guild_ban_remove,
61
61
  "GUILD_ROLE_UPDATE": self._parse_guild_role_update,
62
62
  "TYPING_START": self._parse_typing_start,
63
+ "VOICE_STATE_UPDATE": self._parse_voice_state_update,
63
64
  }
64
65
 
65
66
  def _parse_message_create(self, data: Dict[str, Any]) -> Message:
@@ -111,6 +112,13 @@ class EventDispatcher:
111
112
 
112
113
  return TypingStart(data, client_instance=self._client)
113
114
 
115
+ def _parse_voice_state_update(self, data: Dict[str, Any]):
116
+ """Parses raw VOICE_STATE_UPDATE data into a VoiceStateUpdate object."""
117
+
118
+ from .models import VoiceStateUpdate
119
+
120
+ return VoiceStateUpdate(data, client_instance=self._client)
121
+
114
122
  def _parse_message_reaction(self, data: Dict[str, Any]):
115
123
  """Parses raw reaction data into a Reaction object."""
116
124
 
disagreement/http.py CHANGED
@@ -839,9 +839,13 @@ class HTTPClient:
839
839
  use_auth_header=False,
840
840
  )
841
841
 
842
- async def get_user(self, user_id: "Snowflake") -> Dict[str, Any]:
843
- """Fetches a user object for a given user ID."""
844
- return await self.request("GET", f"/users/{user_id}")
842
+ async def get_user(self, user_id: "Snowflake") -> Dict[str, Any]:
843
+ """Fetches a user object for a given user ID."""
844
+ return await self.request("GET", f"/users/{user_id}")
845
+
846
+ async def get_current_user_guilds(self) -> List[Dict[str, Any]]:
847
+ """Returns the guilds the current user is in."""
848
+ return await self.request("GET", "/users/@me/guilds")
845
849
 
846
850
  async def get_guild_member(
847
851
  self, guild_id: "Snowflake", user_id: "Snowflake"
disagreement/models.py CHANGED
@@ -106,14 +106,21 @@ class Message:
106
106
  ]
107
107
  else:
108
108
  self.components = None
109
- self.attachments: List[Attachment] = [
110
- Attachment(a) for a in data.get("attachments", [])
111
- ]
112
- self.pinned: bool = data.get("pinned", False)
113
- # Add other fields as needed, e.g., attachments, embeds, reactions, etc.
114
- # self.mentions: List[User] = [User(u) for u in data.get("mentions", [])]
115
- # self.mention_roles: List[str] = data.get("mention_roles", [])
116
- # self.mention_everyone: bool = data.get("mention_everyone", False)
109
+ self.attachments: List[Attachment] = [
110
+ Attachment(a) for a in data.get("attachments", [])
111
+ ]
112
+ self.pinned: bool = data.get("pinned", False)
113
+ # Add other fields as needed, e.g., attachments, embeds, reactions, etc.
114
+ # self.mentions: List[User] = [User(u) for u in data.get("mentions", [])]
115
+ # self.mention_roles: List[str] = data.get("mention_roles", [])
116
+ # self.mention_everyone: bool = data.get("mention_everyone", False)
117
+
118
+ @property
119
+ def jump_url(self) -> str:
120
+ """Return a URL that jumps to this message in the Discord client."""
121
+
122
+ guild_or_dm = self.guild_id or "@me"
123
+ return f"https://discord.com/channels/{guild_or_dm}/{self.channel_id}/{self.id}"
117
124
 
118
125
  @property
119
126
  def clean_content(self) -> str:
@@ -624,14 +631,31 @@ class File:
624
631
  self.data = data
625
632
 
626
633
 
627
- class AllowedMentions:
634
+ class AllowedMentions:
628
635
  """Represents allowed mentions for a message or interaction response."""
629
636
 
630
- def __init__(self, data: Dict[str, Any]):
631
- self.parse: List[str] = data.get("parse", [])
632
- self.roles: List[str] = data.get("roles", [])
633
- self.users: List[str] = data.get("users", [])
634
- self.replied_user: bool = data.get("replied_user", False)
637
+ def __init__(self, data: Dict[str, Any]):
638
+ self.parse: List[str] = data.get("parse", [])
639
+ self.roles: List[str] = data.get("roles", [])
640
+ self.users: List[str] = data.get("users", [])
641
+ self.replied_user: bool = data.get("replied_user", False)
642
+
643
+ @classmethod
644
+ def all(cls) -> "AllowedMentions":
645
+ """Return an instance allowing all mention types."""
646
+
647
+ return cls(
648
+ {
649
+ "parse": ["users", "roles", "everyone"],
650
+ "replied_user": True,
651
+ }
652
+ )
653
+
654
+ @classmethod
655
+ def none(cls) -> "AllowedMentions":
656
+ """Return an instance disallowing all mentions."""
657
+
658
+ return cls({"parse": [], "replied_user": False})
635
659
 
636
660
  def to_dict(self) -> Dict[str, Any]:
637
661
  payload: Dict[str, Any] = {"parse": self.parse}
@@ -2467,7 +2491,7 @@ class PresenceUpdate:
2467
2491
  return f"<PresenceUpdate user_id='{self.user.id}' guild_id='{self.guild_id}' status='{self.status}'>"
2468
2492
 
2469
2493
 
2470
- class TypingStart:
2494
+ class TypingStart:
2471
2495
  """Represents a TYPING_START event."""
2472
2496
 
2473
2497
  def __init__(
@@ -2480,10 +2504,39 @@ class TypingStart:
2480
2504
  self.timestamp: int = data["timestamp"]
2481
2505
  self.member: Optional[Member] = (
2482
2506
  Member(data["member"], client_instance) if data.get("member") else None
2483
- )
2484
-
2485
- def __repr__(self) -> str:
2486
- return f"<TypingStart channel_id='{self.channel_id}' user_id='{self.user_id}'>"
2507
+ )
2508
+
2509
+ def __repr__(self) -> str:
2510
+ return f"<TypingStart channel_id='{self.channel_id}' user_id='{self.user_id}'>"
2511
+
2512
+
2513
+ class VoiceStateUpdate:
2514
+ """Represents a VOICE_STATE_UPDATE event."""
2515
+
2516
+ def __init__(
2517
+ self, data: Dict[str, Any], client_instance: Optional["Client"] = None
2518
+ ):
2519
+ self._client = client_instance
2520
+ self.guild_id: Optional[str] = data.get("guild_id")
2521
+ self.channel_id: Optional[str] = data.get("channel_id")
2522
+ self.user_id: str = data["user_id"]
2523
+ self.member: Optional[Member] = (
2524
+ Member(data["member"], client_instance) if data.get("member") else None
2525
+ )
2526
+ self.session_id: str = data["session_id"]
2527
+ self.deaf: bool = data.get("deaf", False)
2528
+ self.mute: bool = data.get("mute", False)
2529
+ self.self_deaf: bool = data.get("self_deaf", False)
2530
+ self.self_mute: bool = data.get("self_mute", False)
2531
+ self.self_stream: Optional[bool] = data.get("self_stream")
2532
+ self.self_video: bool = data.get("self_video", False)
2533
+ self.suppress: bool = data.get("suppress", False)
2534
+
2535
+ def __repr__(self) -> str:
2536
+ return (
2537
+ f"<VoiceStateUpdate guild_id='{self.guild_id}' user_id='{self.user_id}' "
2538
+ f"channel_id='{self.channel_id}'>"
2539
+ )
2487
2540
 
2488
2541
 
2489
2542
  class Reaction:
@@ -259,5 +259,20 @@ class VoiceClient:
259
259
  self._udp.close()
260
260
  if self._udp_receive_thread:
261
261
  self._udp_receive_thread.join(timeout=1)
262
- if self._sink:
263
- self._sink.close()
262
+ if self._sink:
263
+ self._sink.close()
264
+
265
+ async def __aenter__(self) -> "VoiceClient":
266
+ """Enter the context manager by connecting to the voice gateway."""
267
+ await self.connect()
268
+ return self
269
+
270
+ async def __aexit__(
271
+ self,
272
+ exc_type: Optional[type],
273
+ exc: Optional[BaseException],
274
+ tb: Optional[BaseException],
275
+ ) -> bool:
276
+ """Exit the context manager and close the connection."""
277
+ await self.close()
278
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: A Python library for the Discord API.
5
5
  Author-email: Slipstream <me@slipstreamm.dev>
6
6
  License: BSD 3-Clause
@@ -46,6 +46,7 @@ A Python library for interacting with the Discord API, with a focus on bot devel
46
46
  - Gateway and HTTP API clients
47
47
  - Slash command framework
48
48
  - Message component helpers
49
+ - `Message.jump_url` property for quick links to messages
49
50
  - Built-in caching layer
50
51
  - Experimental voice support
51
52
  - Helpful error handling utilities
@@ -153,9 +154,10 @@ session parameter supported by ``aiohttp``.
153
154
  Specify default mention behaviour for all outgoing messages when constructing the client:
154
155
 
155
156
  ```python
157
+ from disagreement.models import AllowedMentions
156
158
  client = disagreement.Client(
157
159
  token=token,
158
- allowed_mentions={"parse": [], "replied_user": False},
160
+ allowed_mentions=AllowedMentions.none().to_dict(),
159
161
  )
160
162
  ```
161
163
 
@@ -192,6 +194,14 @@ guild = await client.fetch_guild("123456789012345678")
192
194
  roles = await client.fetch_roles(guild.id)
193
195
  ```
194
196
 
197
+ Call `Client.fetch_guilds` to list all guilds the current user has access to.
198
+
199
+ ```python
200
+ guilds = await client.fetch_guilds()
201
+ for g in guilds:
202
+ print(g.name)
203
+ ```
204
+
195
205
  ## Sharding
196
206
 
197
207
  To run your bot across multiple gateway shards, pass ``shard_count`` when creating
@@ -1,21 +1,21 @@
1
- disagreement/__init__.py,sha256=4rWCD-jlCX7cGKi7JpXzFGr4aHgM6N2lBuwrsQ7VA44,1137
1
+ disagreement/__init__.py,sha256=SEe8hwL_hzJj8c2gjFJG35OV-f-10owc4NbpdC3gnu0,1646
2
2
  disagreement/audio.py,sha256=erBaupz-Hw8LR-5mGLDvhphCDAQeWk0ZBA0AMVwDUIM,4360
3
- disagreement/cache.py,sha256=srmaw-x8iiqU53eh33JgLLkM-K-TgFnvOS2lYO7Z2FI,2866
3
+ disagreement/cache.py,sha256=9yit7gIro0nEngBQUsBLHN2gGWTZ2EYiBMrfgcu5LVM,3157
4
4
  disagreement/caching.py,sha256=KEicB8fIBRwc5Ifl1CCHHnPu-NfAvqdfArjxFuV557g,3841
5
- disagreement/client.py,sha256=RBiprWFfW02Wkt8FwHaYvJfcK4vOR559Fn5p26ab9ME,67441
5
+ disagreement/client.py,sha256=W-8DwXxcZEAjaIZfZVKy49CZMhXDf4-WIrBnSvd4cFc,68285
6
6
  disagreement/color.py,sha256=0RumZU9geS51rmmywwotmkXFc8RyQghOviRGGrHmvW4,4495
7
7
  disagreement/components.py,sha256=tEYJ2RHVpIFtZuPPxZ0v8ssUw_x7ybhYBzHNsRiXXvU,5250
8
8
  disagreement/enums.py,sha256=F_DHgeAnnw17ILswOuxPR6G1OimL6aeuxIEAni1XPyY,11187
9
9
  disagreement/error_handler.py,sha256=rj9AiJhRLPNAaAY6Gj0KQXgKNfPsgnZUtyTljclybD8,1161
10
10
  disagreement/errors.py,sha256=Zi7x_hgGXCDw5ht2HpcQDba4NQbXkBbjQIVhU9RVyhE,32236
11
- disagreement/event_dispatcher.py,sha256=o-PgfOYEwlCbmrWeLuycp-QT6tlXF2JEU3Dyv0VFZ0c,11419
11
+ disagreement/event_dispatcher.py,sha256=0vCLYRPSihuf1qmLBY9KFjj85bVWUxB8s7uhJvMbyy4,11745
12
12
  disagreement/gateway.py,sha256=7ccRygL1gnd_w8DldsLFxreZy1iCdw6Nw65_UW8Ulz8,26831
13
- disagreement/http.py,sha256=iYmX6QjBEkcZlHJTefJ-tHHYp8BV4TTQYE-vaHGjfUk,51729
13
+ disagreement/http.py,sha256=IRaEhhQvfnSX5HM_2r3OTvrMdPK-suhoi7l2Wsl3X-c,51915
14
14
  disagreement/hybrid_context.py,sha256=VYCmcreTZdPBU9v-Cy48W38vgWO2t8nM2ulC6_z4HjU,1095
15
15
  disagreement/i18n.py,sha256=1L4rcFuKP0XjHk9dVwbNh4BkLk2ZlxxZ_-tecGWa9S0,718
16
16
  disagreement/interactions.py,sha256=moN2iQEE9ntC-Y2Q7uxUqoBBRYHegNwB00w-2UD30q4,21703
17
17
  disagreement/logging_config.py,sha256=4q6baQPE6X_0lfaBTFMU1uqc03x5SbJqo2hsApdDFac,686
18
- disagreement/models.py,sha256=ohpMS03K9Hs9lJ2uWLG_0P3VTJr8sSy9dNsc3uRerpM,97981
18
+ disagreement/models.py,sha256=q6txqdQ0EpUqeZbn24BYRqDl3mJ1lO2qPnnB4mnhyFU,99839
19
19
  disagreement/oauth.py,sha256=TfDdCwg1J7osM9wDi61dtNBA5BrQk5DeQrrHsYycH34,2810
20
20
  disagreement/permissions.py,sha256=7g5cIlg-evHXOL0-pmtT5EwqcB-stXot1HZSLz724sE,3008
21
21
  disagreement/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -23,7 +23,7 @@ disagreement/rate_limiter.py,sha256=ubwR_UTPs2MotipBdtqpgwQKx0IHt2I3cdfFcXTFv7g,
23
23
  disagreement/shard_manager.py,sha256=gE7562izYcN-5jTDWUP9kIFX5gffRjIryqNxk1mtnSM,2128
24
24
  disagreement/typing.py,sha256=_1oFWfZ4HyH5Q3bnF7CO24s79z-3_B5Qb69kWiwLhhU,1242
25
25
  disagreement/utils.py,sha256=mz7foTCOAmUv9n8EcdZeiFarwqB14xHOG8o0p8tFuKA,2014
26
- disagreement/voice_client.py,sha256=AgBhId6syetP4wwHq47IgGlfNIqtLUt_5nbEE-c6KtQ,9250
26
+ disagreement/voice_client.py,sha256=mn7wiE_HzUibaTPY-xCP7b8AkOx4ioeR7zF2YUqC7ok,9700
27
27
  disagreement/ext/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
28
28
  disagreement/ext/loader.py,sha256=9_uULvNAa-a6UiaeQhWglwgIrHEPKbf9bnWtSL1KV5Q,1408
29
29
  disagreement/ext/tasks.py,sha256=b14KI-btikbrjPlD76md3Ggt6znrxPqr7TDarU4PYBg,7269
@@ -48,8 +48,8 @@ disagreement/ui/item.py,sha256=bm-EmQEZpe8Kt8JrRw-o0uQdccgjErORcFsBqaXOcV8,1112
48
48
  disagreement/ui/modal.py,sha256=w0ZEVslXzx2-RWUP4jdVB54zDuT81jpueVWZ70byFnI,4137
49
49
  disagreement/ui/select.py,sha256=XjWRlWkA09QZaDDLn-wDDOWIuj0Mb4VCWJEOAaExZXw,3018
50
50
  disagreement/ui/view.py,sha256=UdOaoSe0NZXZMjOtM8CLCPcDHVf6Dn2yr2PHLSvoJsg,5834
51
- disagreement-0.4.1.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
52
- disagreement-0.4.1.dist-info/METADATA,sha256=yKEkXoTqMFuBVFKGIKQ6d39ExMuS24MiniEJ3ggyRkY,6322
53
- disagreement-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
- disagreement-0.4.1.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
55
- disagreement-0.4.1.dist-info/RECORD,,
51
+ disagreement-0.4.2.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
52
+ disagreement-0.4.2.dist-info/METADATA,sha256=Y4OVIt8mHGWsb9fuyL51QiVXVrObftm4g7TQVEATmL0,6590
53
+ disagreement-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
54
+ disagreement-0.4.2.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
55
+ disagreement-0.4.2.dist-info/RECORD,,