disagreement 0.4.0__tar.gz → 0.4.2__tar.gz

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 (166) hide show
  1. {disagreement-0.4.0/disagreement.egg-info → disagreement-0.4.2}/PKG-INFO +13 -2
  2. {disagreement-0.4.0 → disagreement-0.4.2}/README.md +11 -1
  3. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/__init__.py +30 -1
  4. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/cache.py +9 -1
  5. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/client.py +38 -12
  6. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/event_dispatcher.py +8 -0
  7. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/http.py +7 -3
  8. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/models.py +72 -19
  9. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/voice_client.py +17 -2
  10. {disagreement-0.4.0 → disagreement-0.4.2/disagreement.egg-info}/PKG-INFO +13 -2
  11. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement.egg-info/SOURCES.txt +1 -0
  12. {disagreement-0.4.0 → disagreement-0.4.2}/docs/events.md +12 -2
  13. {disagreement-0.4.0 → disagreement-0.4.2}/docs/http_client.md +11 -0
  14. {disagreement-0.4.0 → disagreement-0.4.2}/docs/introduction.md +11 -1
  15. {disagreement-0.4.0 → disagreement-0.4.2}/docs/mentions.md +6 -1
  16. {disagreement-0.4.0 → disagreement-0.4.2}/docs/message_history.md +3 -0
  17. {disagreement-0.4.0 → disagreement-0.4.2}/docs/voice_client.md +12 -10
  18. disagreement-0.4.2/examples/moderation_bot.py +89 -0
  19. {disagreement-0.4.0 → disagreement-0.4.2}/pyproject.toml +2 -1
  20. disagreement-0.4.2/tests/test_cache.py +67 -0
  21. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_presence_and_typing.py +20 -1
  22. disagreement-0.4.0/tests/test_cache.py +0 -28
  23. {disagreement-0.4.0 → disagreement-0.4.2}/LICENSE +0 -0
  24. {disagreement-0.4.0 → disagreement-0.4.2}/MANIFEST.in +0 -0
  25. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/audio.py +0 -0
  26. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/caching.py +0 -0
  27. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/color.py +0 -0
  28. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/components.py +0 -0
  29. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/enums.py +0 -0
  30. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/error_handler.py +0 -0
  31. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/errors.py +0 -0
  32. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/__init__.py +0 -0
  33. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/app_commands/__init__.py +0 -0
  34. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/app_commands/commands.py +0 -0
  35. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/app_commands/context.py +0 -0
  36. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/app_commands/converters.py +0 -0
  37. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/app_commands/decorators.py +0 -0
  38. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/app_commands/handler.py +0 -0
  39. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/app_commands/hybrid.py +0 -0
  40. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/commands/__init__.py +0 -0
  41. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/commands/cog.py +0 -0
  42. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/commands/converters.py +0 -0
  43. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/commands/core.py +0 -0
  44. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/commands/decorators.py +0 -0
  45. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/commands/errors.py +0 -0
  46. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/commands/help.py +0 -0
  47. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/commands/view.py +0 -0
  48. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/loader.py +0 -0
  49. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ext/tasks.py +0 -0
  50. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/gateway.py +0 -0
  51. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/hybrid_context.py +0 -0
  52. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/i18n.py +0 -0
  53. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/interactions.py +0 -0
  54. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/logging_config.py +0 -0
  55. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/oauth.py +0 -0
  56. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/permissions.py +0 -0
  57. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/py.typed +0 -0
  58. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/rate_limiter.py +0 -0
  59. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/shard_manager.py +0 -0
  60. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/typing.py +0 -0
  61. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ui/__init__.py +0 -0
  62. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ui/button.py +0 -0
  63. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ui/item.py +0 -0
  64. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ui/modal.py +0 -0
  65. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ui/select.py +0 -0
  66. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/ui/view.py +0 -0
  67. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement/utils.py +0 -0
  68. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement.egg-info/dependency_links.txt +0 -0
  69. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement.egg-info/requires.txt +0 -0
  70. {disagreement-0.4.0 → disagreement-0.4.2}/disagreement.egg-info/top_level.txt +0 -0
  71. {disagreement-0.4.0 → disagreement-0.4.2}/docs/audit_logs.md +0 -0
  72. {disagreement-0.4.0 → disagreement-0.4.2}/docs/caching.md +0 -0
  73. {disagreement-0.4.0 → disagreement-0.4.2}/docs/commands.md +0 -0
  74. {disagreement-0.4.0 → disagreement-0.4.2}/docs/context_menus.md +0 -0
  75. {disagreement-0.4.0 → disagreement-0.4.2}/docs/converters.md +0 -0
  76. {disagreement-0.4.0 → disagreement-0.4.2}/docs/embeds.md +0 -0
  77. {disagreement-0.4.0 → disagreement-0.4.2}/docs/extension_loader.md +0 -0
  78. {disagreement-0.4.0 → disagreement-0.4.2}/docs/gateway.md +0 -0
  79. {disagreement-0.4.0 → disagreement-0.4.2}/docs/hybrid_context.md +0 -0
  80. {disagreement-0.4.0 → disagreement-0.4.2}/docs/i18n.md +0 -0
  81. {disagreement-0.4.0 → disagreement-0.4.2}/docs/index.md +0 -0
  82. {disagreement-0.4.0 → disagreement-0.4.2}/docs/invites.md +0 -0
  83. {disagreement-0.4.0 → disagreement-0.4.2}/docs/oauth2.md +0 -0
  84. {disagreement-0.4.0 → disagreement-0.4.2}/docs/permissions.md +0 -0
  85. {disagreement-0.4.0 → disagreement-0.4.2}/docs/presence.md +0 -0
  86. {disagreement-0.4.0 → disagreement-0.4.2}/docs/rate_limiter.md +0 -0
  87. {disagreement-0.4.0 → disagreement-0.4.2}/docs/reactions.md +0 -0
  88. {disagreement-0.4.0 → disagreement-0.4.2}/docs/scheduled_events.md +0 -0
  89. {disagreement-0.4.0 → disagreement-0.4.2}/docs/sharding.md +0 -0
  90. {disagreement-0.4.0 → disagreement-0.4.2}/docs/slash_commands.md +0 -0
  91. {disagreement-0.4.0 → disagreement-0.4.2}/docs/task_loop.md +0 -0
  92. {disagreement-0.4.0 → disagreement-0.4.2}/docs/threads.md +0 -0
  93. {disagreement-0.4.0 → disagreement-0.4.2}/docs/typing_indicator.md +0 -0
  94. {disagreement-0.4.0 → disagreement-0.4.2}/docs/using_components.md +0 -0
  95. {disagreement-0.4.0 → disagreement-0.4.2}/docs/voice_features.md +0 -0
  96. {disagreement-0.4.0 → disagreement-0.4.2}/docs/webhooks.md +0 -0
  97. {disagreement-0.4.0 → disagreement-0.4.2}/examples/basic_bot.py +0 -0
  98. {disagreement-0.4.0 → disagreement-0.4.2}/examples/component_bot.py +0 -0
  99. {disagreement-0.4.0 → disagreement-0.4.2}/examples/context_menus.py +0 -0
  100. {disagreement-0.4.0 → disagreement-0.4.2}/examples/example_from_readme.py +0 -0
  101. {disagreement-0.4.0 → disagreement-0.4.2}/examples/extension_management.py +0 -0
  102. {disagreement-0.4.0 → disagreement-0.4.2}/examples/hybrid_bot.py +0 -0
  103. {disagreement-0.4.0 → disagreement-0.4.2}/examples/message_history.py +0 -0
  104. {disagreement-0.4.0 → disagreement-0.4.2}/examples/modal_command.py +0 -0
  105. {disagreement-0.4.0 → disagreement-0.4.2}/examples/modal_send.py +0 -0
  106. {disagreement-0.4.0 → disagreement-0.4.2}/examples/reactions.py +0 -0
  107. {disagreement-0.4.0 → disagreement-0.4.2}/examples/sample_extension.py +0 -0
  108. {disagreement-0.4.0 → disagreement-0.4.2}/examples/sharded_bot.py +0 -0
  109. {disagreement-0.4.0 → disagreement-0.4.2}/examples/task_loop.py +0 -0
  110. {disagreement-0.4.0 → disagreement-0.4.2}/examples/typing_indicator.py +0 -0
  111. {disagreement-0.4.0 → disagreement-0.4.2}/examples/voice_bot.py +0 -0
  112. {disagreement-0.4.0 → disagreement-0.4.2}/setup.cfg +0 -0
  113. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_additional_converters.py +0 -0
  114. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_channel_permissions.py +0 -0
  115. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_client_context_manager.py +0 -0
  116. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_client_message_cache.py +0 -0
  117. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_color.py +0 -0
  118. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_color_acceptance.py +0 -0
  119. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_command_checks.py +0 -0
  120. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_components_factory.py +0 -0
  121. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_context.py +0 -0
  122. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_context_menus.py +0 -0
  123. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_converter_registration.py +0 -0
  124. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_converters.py +0 -0
  125. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_embed_methods.py +0 -0
  126. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_error_handler.py +0 -0
  127. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_errors.py +0 -0
  128. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_event_dispatcher.py +0 -0
  129. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_event_error_hook.py +0 -0
  130. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_extension_loader.py +0 -0
  131. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_gateway_backoff.py +0 -0
  132. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_gateway_intent.py +0 -0
  133. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_help_command.py +0 -0
  134. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_http_rate_limit.py +0 -0
  135. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_http_reactions.py +0 -0
  136. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_hybrid_context.py +0 -0
  137. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_i18n.py +0 -0
  138. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_interaction.py +0 -0
  139. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_logging_config.py +0 -0
  140. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_max_concurrency.py +0 -0
  141. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_member.py +0 -0
  142. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_message_clean_content.py +0 -0
  143. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_message_pager.py +0 -0
  144. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_modal_send.py +0 -0
  145. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_modals.py +0 -0
  146. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_oauth.py +0 -0
  147. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_permissions.py +0 -0
  148. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_presence_update.py +0 -0
  149. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_rate_limiter.py +0 -0
  150. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_reactions.py +0 -0
  151. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_send_files.py +0 -0
  152. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_sharding.py +0 -0
  153. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_slash_contexts.py +0 -0
  154. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_stage_instance.py +0 -0
  155. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_tasks_extension.py +0 -0
  156. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_templates.py +0 -0
  157. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_textchannel_history.py +0 -0
  158. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_textchannel_purge.py +0 -0
  159. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_typing_indicator.py +0 -0
  160. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_ui.py +0 -0
  161. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_utils.py +0 -0
  162. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_view_layout.py +0 -0
  163. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_voice_client.py +0 -0
  164. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_wait_for.py +0 -0
  165. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_webhooks.py +0 -0
  166. {disagreement-0.4.0 → disagreement-0.4.2}/tests/test_widget.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.4.0
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
7
7
  Project-URL: Homepage, https://github.com/Slipstreamm/disagreement
8
8
  Project-URL: Issues, https://github.com/Slipstreamm/disagreement/issues
9
+ Project-URL: Documentation, https://disagreement.xyz/
9
10
  Keywords: discord,api,bot,async,aiohttp
10
11
  Classifier: Development Status :: 4 - Beta
11
12
  Classifier: Intended Audience :: Developers
@@ -45,6 +46,7 @@ A Python library for interacting with the Discord API, with a focus on bot devel
45
46
  - Gateway and HTTP API clients
46
47
  - Slash command framework
47
48
  - Message component helpers
49
+ - `Message.jump_url` property for quick links to messages
48
50
  - Built-in caching layer
49
51
  - Experimental voice support
50
52
  - Helpful error handling utilities
@@ -152,9 +154,10 @@ session parameter supported by ``aiohttp``.
152
154
  Specify default mention behaviour for all outgoing messages when constructing the client:
153
155
 
154
156
  ```python
157
+ from disagreement.models import AllowedMentions
155
158
  client = disagreement.Client(
156
159
  token=token,
157
- allowed_mentions={"parse": [], "replied_user": False},
160
+ allowed_mentions=AllowedMentions.none().to_dict(),
158
161
  )
159
162
  ```
160
163
 
@@ -191,6 +194,14 @@ guild = await client.fetch_guild("123456789012345678")
191
194
  roles = await client.fetch_roles(guild.id)
192
195
  ```
193
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
+
194
205
  ## Sharding
195
206
 
196
207
  To run your bot across multiple gateway shards, pass ``shard_count`` when creating
@@ -11,6 +11,7 @@ A Python library for interacting with the Discord API, with a focus on bot devel
11
11
  - Gateway and HTTP API clients
12
12
  - Slash command framework
13
13
  - Message component helpers
14
+ - `Message.jump_url` property for quick links to messages
14
15
  - Built-in caching layer
15
16
  - Experimental voice support
16
17
  - Helpful error handling utilities
@@ -118,9 +119,10 @@ session parameter supported by ``aiohttp``.
118
119
  Specify default mention behaviour for all outgoing messages when constructing the client:
119
120
 
120
121
  ```python
122
+ from disagreement.models import AllowedMentions
121
123
  client = disagreement.Client(
122
124
  token=token,
123
- allowed_mentions={"parse": [], "replied_user": False},
125
+ allowed_mentions=AllowedMentions.none().to_dict(),
124
126
  )
125
127
  ```
126
128
 
@@ -157,6 +159,14 @@ guild = await client.fetch_guild("123456789012345678")
157
159
  roles = await client.fetch_roles(guild.id)
158
160
  ```
159
161
 
162
+ Call `Client.fetch_guilds` to list all guilds the current user has access to.
163
+
164
+ ```python
165
+ guilds = await client.fetch_guilds()
166
+ for g in guilds:
167
+ print(g.name)
168
+ ```
169
+
160
170
  ## Sharding
161
171
 
162
172
  To run your bot across multiple gateway shards, pass ``shard_count`` when creating
@@ -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.0"
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)
@@ -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
 
@@ -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
 
@@ -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"
@@ -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,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.4.0
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
7
7
  Project-URL: Homepage, https://github.com/Slipstreamm/disagreement
8
8
  Project-URL: Issues, https://github.com/Slipstreamm/disagreement/issues
9
+ Project-URL: Documentation, https://disagreement.xyz/
9
10
  Keywords: discord,api,bot,async,aiohttp
10
11
  Classifier: Development Status :: 4 - Beta
11
12
  Classifier: Intended Audience :: Developers
@@ -45,6 +46,7 @@ A Python library for interacting with the Discord API, with a focus on bot devel
45
46
  - Gateway and HTTP API clients
46
47
  - Slash command framework
47
48
  - Message component helpers
49
+ - `Message.jump_url` property for quick links to messages
48
50
  - Built-in caching layer
49
51
  - Experimental voice support
50
52
  - Helpful error handling utilities
@@ -152,9 +154,10 @@ session parameter supported by ``aiohttp``.
152
154
  Specify default mention behaviour for all outgoing messages when constructing the client:
153
155
 
154
156
  ```python
157
+ from disagreement.models import AllowedMentions
155
158
  client = disagreement.Client(
156
159
  token=token,
157
- allowed_mentions={"parse": [], "replied_user": False},
160
+ allowed_mentions=AllowedMentions.none().to_dict(),
158
161
  )
159
162
  ```
160
163
 
@@ -191,6 +194,14 @@ guild = await client.fetch_guild("123456789012345678")
191
194
  roles = await client.fetch_roles(guild.id)
192
195
  ```
193
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
+
194
205
  ## Sharding
195
206
 
196
207
  To run your bot across multiple gateway shards, pass ``shard_count`` when creating
@@ -99,6 +99,7 @@ examples/hybrid_bot.py
99
99
  examples/message_history.py
100
100
  examples/modal_command.py
101
101
  examples/modal_send.py
102
+ examples/moderation_bot.py
102
103
  examples/reactions.py
103
104
  examples/sample_extension.py
104
105
  examples/sharded_bot.py
@@ -1,7 +1,7 @@
1
1
  # Events
2
2
 
3
- Disagreement dispatches Gateway events to asynchronous callbacks. Handlers can be registered with `@client.event` or `client.on_event`.
4
- Listeners may be removed later using `EventDispatcher.unregister(event_name, coro)`.
3
+ Disagreement dispatches Gateway events to asynchronous callbacks. Handlers can be registered with `@client.event`, `client.on_event`, or `client.add_listener(event_name, coro)`.
4
+ Listeners may be removed later using `client.remove_listener(event_name, coro)` or `EventDispatcher.unregister(event_name, coro)`.
5
5
 
6
6
  ## Raw Events
7
7
 
@@ -131,3 +131,13 @@ a dictionary with the shard ID.
131
131
  async def on_shard_resume(info: dict):
132
132
  ...
133
133
  ```
134
+
135
+ ## VOICE_STATE_UPDATE
136
+
137
+ Triggered when a user's voice connection state changes, such as joining or leaving a voice channel. The callback receives a `VoiceStateUpdate` model.
138
+
139
+ ```python
140
+ @client.event
141
+ async def on_voice_state_update(state: disagreement.VoiceStateUpdate):
142
+ ...
143
+ ```
@@ -18,3 +18,14 @@ client = Client(
18
18
  These options are passed through to `aiohttp.ClientSession` when the session is
19
19
  created. You can set a proxy URL, provide a custom connector, or supply any
20
20
  other supported session argument.
21
+
22
+ ## Get Current User Guilds
23
+
24
+ The HTTP client can list the guilds the bot user is in:
25
+
26
+ ```python
27
+ from disagreement.http import HTTPClient
28
+
29
+ http = HTTPClient(token="TOKEN")
30
+ guilds = await http.get_current_user_guilds()
31
+ ```
@@ -118,15 +118,19 @@ session parameter supported by ``aiohttp``.
118
118
  Specify default mention behaviour for all outgoing messages when constructing the client:
119
119
 
120
120
  ```python
121
+ from disagreement.models import AllowedMentions
121
122
  client = disagreement.Client(
122
123
  token=token,
123
- allowed_mentions={"parse": [], "replied_user": False},
124
+ allowed_mentions=AllowedMentions.none().to_dict(),
124
125
  )
125
126
  ```
126
127
 
127
128
  This dictionary is used whenever ``send_message`` is called without an explicit
128
129
  ``allowed_mentions`` argument.
129
130
 
131
+ The :class:`AllowedMentions` class offers ``none()`` and ``all()`` helpers for
132
+ quickly generating these configurations.
133
+
130
134
  ### Defining Subcommands with `AppCommandGroup`
131
135
 
132
136
  ```python
@@ -157,6 +161,12 @@ guild = await client.fetch_guild("123456789012345678")
157
161
  roles = await client.fetch_roles(guild.id)
158
162
  ```
159
163
 
164
+ To retrieve all guilds available to the bot, use `Client.fetch_guilds`.
165
+
166
+ ```python
167
+ guilds = await client.fetch_guilds()
168
+ ```
169
+
160
170
  ## Sharding
161
171
 
162
172
  To run your bot across multiple gateway shards, pass ``shard_count`` when creating
@@ -8,15 +8,20 @@ Use the ``allowed_mentions`` parameter of :class:`disagreement.Client` to set a
8
8
  default for all messages:
9
9
 
10
10
  ```python
11
+ from disagreement.models import AllowedMentions
11
12
  client = disagreement.Client(
12
13
  token="YOUR_TOKEN",
13
- allowed_mentions={"parse": [], "replied_user": False},
14
+ allowed_mentions=AllowedMentions.none().to_dict(),
14
15
  )
15
16
  ```
16
17
 
17
18
  When ``Client.send_message`` is called without an explicit ``allowed_mentions``
18
19
  argument this value will be used.
19
20
 
21
+ ``AllowedMentions`` also provides the convenience methods
22
+ ``AllowedMentions.none()`` and ``AllowedMentions.all()`` to quickly create
23
+ common configurations.
24
+
20
25
  ## Next Steps
21
26
 
22
27
  - [Commands](commands.md)
@@ -8,6 +8,9 @@ async for message in channel.history(limit=200):
8
8
  print(message.content)
9
9
  ```
10
10
 
11
+ Each returned `Message` has a ``jump_url`` property that links directly to the
12
+ message in the Discord client.
13
+
11
14
  Pass `before` or `after` to control the range of messages returned. The paginator fetches messages in batches of up to 100 until the limit is reached or Discord returns no more messages.
12
15
 
13
16
  ## Next Steps
@@ -9,15 +9,17 @@ import asyncio
9
9
  import os
10
10
  import disagreement
11
11
 
12
- vc = disagreement.VoiceClient(
13
- os.environ["DISCORD_VOICE_ENDPOINT"],
14
- os.environ["DISCORD_SESSION_ID"],
15
- os.environ["DISCORD_VOICE_TOKEN"],
16
- int(os.environ["DISCORD_GUILD_ID"]),
17
- int(os.environ["DISCORD_USER_ID"]),
18
- )
19
-
20
- asyncio.run(vc.connect())
12
+ async def main():
13
+ async with disagreement.VoiceClient(
14
+ os.environ["DISCORD_VOICE_ENDPOINT"],
15
+ os.environ["DISCORD_SESSION_ID"],
16
+ os.environ["DISCORD_VOICE_TOKEN"],
17
+ int(os.environ["DISCORD_GUILD_ID"]),
18
+ int(os.environ["DISCORD_USER_ID"]),
19
+ ) as vc:
20
+ await vc.send_audio_frame(b"...")
21
+
22
+ asyncio.run(main())
21
23
  ```
22
24
 
23
25
  After connecting you can send raw Opus frames:
@@ -41,7 +43,7 @@ You can switch sources while connected:
41
43
  await vc.play(FFmpegAudioSource("other.mp3"))
42
44
  ```
43
45
 
44
- Call `await vc.close()` when finished.
46
+ The connection will be closed automatically when leaving the `async with` block.
45
47
 
46
48
  ## Fetching Available Voice Regions
47
49