disagreement 0.3.0b1__tar.gz → 0.4.0__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 (168) hide show
  1. {disagreement-0.3.0b1/disagreement.egg-info → disagreement-0.4.0}/PKG-INFO +32 -6
  2. {disagreement-0.3.0b1 → disagreement-0.4.0}/README.md +31 -5
  3. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/__init__.py +2 -4
  4. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/audio.py +25 -5
  5. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/cache.py +12 -3
  6. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/caching.py +15 -14
  7. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/client.py +86 -52
  8. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/enums.py +10 -3
  9. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/error_handler.py +5 -1
  10. disagreement-0.4.0/disagreement/errors.py +1455 -0
  11. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/event_dispatcher.py +1 -3
  12. disagreement-0.4.0/disagreement/ext/__init__.py +1 -0
  13. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/app_commands/__init__.py +0 -2
  14. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/app_commands/commands.py +0 -2
  15. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/app_commands/context.py +0 -2
  16. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/app_commands/converters.py +2 -4
  17. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/app_commands/decorators.py +5 -7
  18. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/app_commands/handler.py +1 -3
  19. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/app_commands/hybrid.py +0 -2
  20. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/commands/__init__.py +0 -2
  21. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/commands/cog.py +0 -2
  22. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/commands/converters.py +16 -5
  23. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/commands/core.py +52 -14
  24. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/commands/decorators.py +3 -7
  25. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/commands/errors.py +0 -2
  26. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/commands/help.py +0 -2
  27. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/commands/view.py +1 -3
  28. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/gateway.py +27 -25
  29. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/http.py +264 -22
  30. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/interactions.py +0 -2
  31. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/models.py +199 -105
  32. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/shard_manager.py +0 -2
  33. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ui/view.py +2 -2
  34. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/voice_client.py +20 -1
  35. {disagreement-0.3.0b1 → disagreement-0.4.0/disagreement.egg-info}/PKG-INFO +32 -6
  36. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement.egg-info/SOURCES.txt +15 -1
  37. disagreement-0.4.0/docs/embeds.md +22 -0
  38. disagreement-0.4.0/docs/hybrid_context.md +14 -0
  39. disagreement-0.4.0/docs/index.md +5 -0
  40. disagreement-0.4.0/docs/introduction.md +186 -0
  41. disagreement-0.4.0/docs/mentions.md +23 -0
  42. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/presence.md +13 -2
  43. disagreement-0.4.0/docs/rate_limiter.md +14 -0
  44. disagreement-0.4.0/docs/reactions.md +62 -0
  45. disagreement-0.4.0/docs/threads.md +18 -0
  46. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/basic_bot.py +7 -2
  47. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/component_bot.py +8 -2
  48. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/context_menus.py +8 -3
  49. disagreement-0.4.0/examples/example_from_readme.py +41 -0
  50. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/extension_management.py +7 -2
  51. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/hybrid_bot.py +8 -3
  52. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/message_history.py +8 -2
  53. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/modal_command.py +8 -2
  54. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/modal_send.py +7 -2
  55. disagreement-0.4.0/examples/reactions.py +148 -0
  56. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/sharded_bot.py +8 -2
  57. disagreement-0.4.0/examples/typing_indicator.py +119 -0
  58. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/voice_bot.py +7 -2
  59. {disagreement-0.3.0b1 → disagreement-0.4.0}/pyproject.toml +1 -1
  60. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_cache.py +11 -0
  61. disagreement-0.4.0/tests/test_client_message_cache.py +23 -0
  62. disagreement-0.4.0/tests/test_embed_methods.py +18 -0
  63. disagreement-0.4.0/tests/test_errors.py +112 -0
  64. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_gateway_backoff.py +2 -2
  65. disagreement-0.4.0/tests/test_message_clean_content.py +23 -0
  66. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_presence_update.py +4 -3
  67. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_voice_client.py +69 -1
  68. disagreement-0.4.0/tests/test_widget.py +50 -0
  69. disagreement-0.3.0b1/disagreement/errors.py +0 -117
  70. disagreement-0.3.0b1/disagreement/ext/__init__.py +0 -0
  71. disagreement-0.3.0b1/docs/reactions.md +0 -32
  72. disagreement-0.3.0b1/tests/test_errors.py +0 -32
  73. {disagreement-0.3.0b1 → disagreement-0.4.0}/LICENSE +0 -0
  74. {disagreement-0.3.0b1 → disagreement-0.4.0}/MANIFEST.in +0 -0
  75. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/color.py +0 -0
  76. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/components.py +0 -0
  77. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/loader.py +0 -0
  78. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ext/tasks.py +0 -0
  79. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/hybrid_context.py +0 -0
  80. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/i18n.py +0 -0
  81. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/logging_config.py +0 -0
  82. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/oauth.py +0 -0
  83. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/permissions.py +0 -0
  84. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/py.typed +0 -0
  85. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/rate_limiter.py +0 -0
  86. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/typing.py +0 -0
  87. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ui/__init__.py +0 -0
  88. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ui/button.py +0 -0
  89. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ui/item.py +0 -0
  90. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ui/modal.py +0 -0
  91. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/ui/select.py +0 -0
  92. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement/utils.py +0 -0
  93. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement.egg-info/dependency_links.txt +0 -0
  94. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement.egg-info/requires.txt +0 -0
  95. {disagreement-0.3.0b1 → disagreement-0.4.0}/disagreement.egg-info/top_level.txt +0 -0
  96. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/audit_logs.md +0 -0
  97. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/caching.md +0 -0
  98. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/commands.md +0 -0
  99. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/context_menus.md +0 -0
  100. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/converters.md +0 -0
  101. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/events.md +0 -0
  102. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/extension_loader.md +0 -0
  103. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/gateway.md +0 -0
  104. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/http_client.md +0 -0
  105. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/i18n.md +0 -0
  106. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/invites.md +0 -0
  107. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/message_history.md +0 -0
  108. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/oauth2.md +0 -0
  109. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/permissions.md +0 -0
  110. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/scheduled_events.md +0 -0
  111. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/sharding.md +0 -0
  112. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/slash_commands.md +0 -0
  113. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/task_loop.md +0 -0
  114. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/typing_indicator.md +0 -0
  115. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/using_components.md +0 -0
  116. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/voice_client.md +0 -0
  117. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/voice_features.md +0 -0
  118. {disagreement-0.3.0b1 → disagreement-0.4.0}/docs/webhooks.md +0 -0
  119. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/sample_extension.py +0 -0
  120. {disagreement-0.3.0b1 → disagreement-0.4.0}/examples/task_loop.py +0 -0
  121. {disagreement-0.3.0b1 → disagreement-0.4.0}/setup.cfg +0 -0
  122. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_additional_converters.py +0 -0
  123. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_channel_permissions.py +0 -0
  124. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_client_context_manager.py +0 -0
  125. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_color.py +0 -0
  126. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_color_acceptance.py +0 -0
  127. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_command_checks.py +0 -0
  128. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_components_factory.py +0 -0
  129. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_context.py +0 -0
  130. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_context_menus.py +0 -0
  131. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_converter_registration.py +0 -0
  132. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_converters.py +0 -0
  133. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_error_handler.py +0 -0
  134. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_event_dispatcher.py +0 -0
  135. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_event_error_hook.py +0 -0
  136. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_extension_loader.py +0 -0
  137. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_gateway_intent.py +0 -0
  138. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_help_command.py +0 -0
  139. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_http_rate_limit.py +0 -0
  140. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_http_reactions.py +0 -0
  141. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_hybrid_context.py +0 -0
  142. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_i18n.py +0 -0
  143. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_interaction.py +0 -0
  144. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_logging_config.py +0 -0
  145. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_max_concurrency.py +0 -0
  146. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_member.py +0 -0
  147. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_message_pager.py +0 -0
  148. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_modal_send.py +0 -0
  149. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_modals.py +0 -0
  150. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_oauth.py +0 -0
  151. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_permissions.py +0 -0
  152. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_presence_and_typing.py +0 -0
  153. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_rate_limiter.py +0 -0
  154. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_reactions.py +0 -0
  155. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_send_files.py +0 -0
  156. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_sharding.py +0 -0
  157. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_slash_contexts.py +0 -0
  158. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_stage_instance.py +0 -0
  159. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_tasks_extension.py +0 -0
  160. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_templates.py +0 -0
  161. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_textchannel_history.py +0 -0
  162. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_textchannel_purge.py +0 -0
  163. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_typing_indicator.py +0 -0
  164. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_ui.py +0 -0
  165. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_utils.py +0 -0
  166. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_view_layout.py +0 -0
  167. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_wait_for.py +0 -0
  168. {disagreement-0.3.0b1 → disagreement-0.4.0}/tests/test_webhooks.py +0 -0
@@ -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
@@ -4,6 +4,9 @@ A Python library for interacting with the Discord API, with a focus on bot devel
4
4
 
5
5
  ## Features
6
6
 
7
+ - Internationalization helpers
8
+ - Hybrid context for commands
9
+ - Built-in rate limiting
7
10
  - Asynchronous design using `aiohttp`
8
11
  - Gateway and HTTP API clients
9
12
  - Slash command framework
@@ -23,6 +26,13 @@ pip install -e .
23
26
 
24
27
  Requires Python 3.10 or newer.
25
28
 
29
+ To run the example scripts, you'll need the `python-dotenv` package to load
30
+ environment variables. Install the development extras with:
31
+
32
+ ```bash
33
+ pip install "disagreement[dev]"
34
+ ```
35
+
26
36
  ## Basic Usage
27
37
 
28
38
  ```python
@@ -31,6 +41,8 @@ import os
31
41
 
32
42
  import disagreement
33
43
  from disagreement.ext import commands
44
+ from dotenv import load_dotenv
45
+ load_dotenv()
34
46
 
35
47
 
36
48
  class Basics(commands.Cog):
@@ -39,18 +51,17 @@ class Basics(commands.Cog):
39
51
 
40
52
  @commands.command()
41
53
  async def ping(self, ctx: commands.CommandContext) -> None:
42
- await ctx.reply("Pong!")
54
+ await ctx.reply(f"Pong! Gateway Latency: {self.client.latency_ms} ms.")
43
55
 
44
56
 
45
57
  token = os.getenv("DISCORD_BOT_TOKEN")
46
58
  if not token:
47
59
  raise RuntimeError("DISCORD_BOT_TOKEN environment variable not set")
48
60
 
49
- client = disagreement.Client(token=token, command_prefix="!")
50
- client.add_cog(Basics(client))
51
-
52
-
61
+ intents = disagreement.GatewayIntent.default() | disagreement.GatewayIntent.MESSAGE_CONTENT
62
+ client = disagreement.Client(token=token, command_prefix="!", intents=intents, mention_replies=True)
53
63
  async def main() -> None:
64
+ client.add_cog(Basics(client))
54
65
  await client.run()
55
66
 
56
67
 
@@ -102,6 +113,20 @@ These options are forwarded to ``HTTPClient`` when it creates the underlying
102
113
  ``aiohttp.ClientSession``. You can specify a custom ``connector`` or any other
103
114
  session parameter supported by ``aiohttp``.
104
115
 
116
+ ### Default Allowed Mentions
117
+
118
+ Specify default mention behaviour for all outgoing messages when constructing the client:
119
+
120
+ ```python
121
+ client = disagreement.Client(
122
+ token=token,
123
+ allowed_mentions={"parse": [], "replied_user": False},
124
+ )
125
+ ```
126
+
127
+ This dictionary is used whenever ``send_message`` is called without an explicit
128
+ ``allowed_mentions`` argument.
129
+
105
130
  ### Defining Subcommands with `AppCommandGroup`
106
131
 
107
132
  ```python
@@ -120,6 +145,7 @@ async def show(ctx: AppCommandContext, key: str):
120
145
  @slash_command(name="set", description="Update a setting.", parent=admin_group)
121
146
  async def set_setting(ctx: AppCommandContext, key: str, value: str):
122
147
  ...
148
+ ```
123
149
  ## Fetching Guilds
124
150
 
125
151
  Use `Client.fetch_guild` to retrieve a guild from the Discord API if it
@@ -1,5 +1,3 @@
1
- # disagreement/__init__.py
2
-
3
1
  """
4
2
  Disagreement
5
3
  ~~~~~~~~~~~~
@@ -14,7 +12,7 @@ __title__ = "disagreement"
14
12
  __author__ = "Slipstream"
15
13
  __license__ = "BSD 3-Clause License"
16
14
  __copyright__ = "Copyright 2025 Slipstream"
17
- __version__ = "0.3.0b1"
15
+ __version__ = "0.4.0"
18
16
 
19
17
  from .client import Client, AutoShardedClient
20
18
  from .models import Message, User, Reaction, AuditLogEntry
@@ -31,7 +29,7 @@ from .errors import (
31
29
  )
32
30
  from .color import Color
33
31
  from .utils import utcnow, message_pager
34
- from .enums import GatewayIntent, GatewayOpcode # Export enums
32
+ from .enums import GatewayIntent, GatewayOpcode
35
33
  from .error_handler import setup_global_error_handler
36
34
  from .hybrid_context import HybridContext
37
35
  from .ext import tasks
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import contextlib
7
7
  import io
8
+ import shlex
8
9
  from typing import Optional, Union
9
10
 
10
11
 
@@ -35,15 +36,27 @@ class FFmpegAudioSource(AudioSource):
35
36
  A filename, URL, or file-like object to read from.
36
37
  """
37
38
 
38
- def __init__(self, source: Union[str, io.BufferedIOBase]):
39
+ def __init__(
40
+ self,
41
+ source: Union[str, io.BufferedIOBase],
42
+ *,
43
+ before_options: Optional[str] = None,
44
+ options: Optional[str] = None,
45
+ volume: float = 1.0,
46
+ ):
39
47
  self.source = source
48
+ self.before_options = before_options
49
+ self.options = options
50
+ self.volume = volume
40
51
  self.process: Optional[asyncio.subprocess.Process] = None
41
52
  self._feeder: Optional[asyncio.Task] = None
42
53
 
43
54
  async def _spawn(self) -> None:
44
55
  if isinstance(self.source, str):
45
- args = [
46
- "ffmpeg",
56
+ args = ["ffmpeg"]
57
+ if self.before_options:
58
+ args += shlex.split(self.before_options)
59
+ args += [
47
60
  "-i",
48
61
  self.source,
49
62
  "-f",
@@ -54,14 +67,18 @@ class FFmpegAudioSource(AudioSource):
54
67
  "2",
55
68
  "pipe:1",
56
69
  ]
70
+ if self.options:
71
+ args += shlex.split(self.options)
57
72
  self.process = await asyncio.create_subprocess_exec(
58
73
  *args,
59
74
  stdout=asyncio.subprocess.PIPE,
60
75
  stderr=asyncio.subprocess.DEVNULL,
61
76
  )
62
77
  else:
63
- args = [
64
- "ffmpeg",
78
+ args = ["ffmpeg"]
79
+ if self.before_options:
80
+ args += shlex.split(self.before_options)
81
+ args += [
65
82
  "-i",
66
83
  "pipe:0",
67
84
  "-f",
@@ -72,6 +89,8 @@ class FFmpegAudioSource(AudioSource):
72
89
  "2",
73
90
  "pipe:1",
74
91
  ]
92
+ if self.options:
93
+ args += shlex.split(self.options)
75
94
  self.process = await asyncio.create_subprocess_exec(
76
95
  *args,
77
96
  stdin=asyncio.subprocess.PIPE,
@@ -115,6 +134,7 @@ class FFmpegAudioSource(AudioSource):
115
134
  with contextlib.suppress(Exception):
116
135
  self.source.close()
117
136
 
137
+
118
138
  class AudioSink:
119
139
  """Abstract base class for audio sinks."""
120
140
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from typing import TYPE_CHECKING, Dict, Generic, Optional, TypeVar
5
+ from collections import OrderedDict
5
6
 
6
7
  if TYPE_CHECKING:
7
8
  from .models import Channel, Guild, Member
@@ -11,15 +12,22 @@ T = TypeVar("T")
11
12
 
12
13
 
13
14
  class Cache(Generic[T]):
14
- """Simple in-memory cache with optional TTL support."""
15
+ """Simple in-memory cache with optional TTL and max size support."""
15
16
 
16
- def __init__(self, ttl: Optional[float] = None) -> None:
17
+ def __init__(
18
+ self, ttl: Optional[float] = None, maxlen: Optional[int] = None
19
+ ) -> None:
17
20
  self.ttl = ttl
18
- self._data: Dict[str, tuple[T, Optional[float]]] = {}
21
+ self.maxlen = maxlen
22
+ self._data: "OrderedDict[str, tuple[T, Optional[float]]]" = OrderedDict()
19
23
 
20
24
  def set(self, key: str, value: T) -> None:
21
25
  expiry = time.monotonic() + self.ttl if self.ttl is not None else None
26
+ if key in self._data:
27
+ self._data.move_to_end(key)
22
28
  self._data[key] = (value, expiry)
29
+ if self.maxlen is not None and len(self._data) > self.maxlen:
30
+ self._data.popitem(last=False)
23
31
 
24
32
  def get(self, key: str) -> Optional[T]:
25
33
  item = self._data.get(key)
@@ -29,6 +37,7 @@ class Cache(Generic[T]):
29
37
  if expiry is not None and expiry < time.monotonic():
30
38
  self.invalidate(key)
31
39
  return None
40
+ self._data.move_to_end(key)
32
41
  return value
33
42
 
34
43
  def invalidate(self, key: str) -> None:
@@ -8,10 +8,10 @@ class _MemberCacheFlagValue:
8
8
  flag: int
9
9
 
10
10
  def __init__(self, func: Callable[[Any], bool]):
11
- self.flag = getattr(func, 'flag', 0)
11
+ self.flag = getattr(func, "flag", 0)
12
12
  self.__doc__ = func.__doc__
13
13
 
14
- def __get__(self, instance: 'MemberCacheFlags', owner: type) -> Any:
14
+ def __get__(self, instance: "MemberCacheFlags", owner: type) -> Any:
15
15
  if instance is None:
16
16
  return self
17
17
  return instance.value & self.flag != 0
@@ -23,23 +23,24 @@ class _MemberCacheFlagValue:
23
23
  instance.value &= ~self.flag
24
24
 
25
25
  def __repr__(self) -> str:
26
- return f'<{self.__class__.__name__} flag={self.flag}>'
26
+ return f"<{self.__class__.__name__} flag={self.flag}>"
27
27
 
28
28
 
29
29
  def flag_value(flag: int) -> Callable[[Callable[[Any], bool]], _MemberCacheFlagValue]:
30
30
  def decorator(func: Callable[[Any], bool]) -> _MemberCacheFlagValue:
31
- setattr(func, 'flag', flag)
31
+ setattr(func, "flag", flag)
32
32
  return _MemberCacheFlagValue(func)
33
+
33
34
  return decorator
34
35
 
35
36
 
36
37
  class MemberCacheFlags:
37
- __slots__ = ('value',)
38
+ __slots__ = ("value",)
38
39
 
39
40
  VALID_FLAGS: ClassVar[Dict[str, int]] = {
40
- 'joined': 1 << 0,
41
- 'voice': 1 << 1,
42
- 'online': 1 << 2,
41
+ "joined": 1 << 0,
42
+ "voice": 1 << 1,
43
+ "online": 1 << 2,
43
44
  }
44
45
  DEFAULT_FLAGS: ClassVar[int] = 1 | 2 | 4
45
46
  ALL_FLAGS: ClassVar[int] = sum(VALID_FLAGS.values())
@@ -48,7 +49,7 @@ class MemberCacheFlags:
48
49
  self.value = self.DEFAULT_FLAGS
49
50
  for key, value in kwargs.items():
50
51
  if key not in self.VALID_FLAGS:
51
- raise TypeError(f'{key!r} is not a valid member cache flag.')
52
+ raise TypeError(f"{key!r} is not a valid member cache flag.")
52
53
  setattr(self, key, value)
53
54
 
54
55
  @classmethod
@@ -67,7 +68,7 @@ class MemberCacheFlags:
67
68
  return hash(self.value)
68
69
 
69
70
  def __repr__(self) -> str:
70
- return f'<MemberCacheFlags value={self.value}>'
71
+ return f"<MemberCacheFlags value={self.value}>"
71
72
 
72
73
  def __iter__(self) -> Iterator[Tuple[str, bool]]:
73
74
  for name in self.VALID_FLAGS:
@@ -92,17 +93,17 @@ class MemberCacheFlags:
92
93
  @classmethod
93
94
  def only_joined(cls) -> MemberCacheFlags:
94
95
  """A factory method that creates a :class:`MemberCacheFlags` with only the `joined` flag enabled."""
95
- return cls._from_value(cls.VALID_FLAGS['joined'])
96
+ return cls._from_value(cls.VALID_FLAGS["joined"])
96
97
 
97
98
  @classmethod
98
99
  def only_voice(cls) -> MemberCacheFlags:
99
100
  """A factory method that creates a :class:`MemberCacheFlags` with only the `voice` flag enabled."""
100
- return cls._from_value(cls.VALID_FLAGS['voice'])
101
+ return cls._from_value(cls.VALID_FLAGS["voice"])
101
102
 
102
103
  @classmethod
103
104
  def only_online(cls) -> MemberCacheFlags:
104
105
  """A factory method that creates a :class:`MemberCacheFlags` with only the `online` flag enabled."""
105
- return cls._from_value(cls.VALID_FLAGS['online'])
106
+ return cls._from_value(cls.VALID_FLAGS["online"])
106
107
 
107
108
  @flag_value(1 << 0)
108
109
  def joined(self) -> bool:
@@ -117,4 +118,4 @@ class MemberCacheFlags:
117
118
  @flag_value(1 << 2)
118
119
  def online(self) -> bool:
119
120
  """Whether to cache members that are online."""
120
- return False
121
+ return False
@@ -1,5 +1,3 @@
1
- # disagreement/client.py
2
-
3
1
  """
4
2
  The main Client class for interacting with the Discord API.
5
3
  """
@@ -36,6 +34,7 @@ from .ext import loader as ext_loader
36
34
  from .interactions import Interaction, Snowflake
37
35
  from .error_handler import setup_global_error_handler
38
36
  from .voice_client import VoiceClient
37
+ from .models import Activity
39
38
 
40
39
  if TYPE_CHECKING:
41
40
  from .models import (
@@ -75,13 +74,21 @@ class Client:
75
74
  intents (Optional[int]): The Gateway Intents to use. Defaults to `GatewayIntent.default()`.
76
75
  You might need to enable privileged intents in your bot's application page.
77
76
  loop (Optional[asyncio.AbstractEventLoop]): The event loop to use for asynchronous operations.
78
- Defaults to `asyncio.get_event_loop()`.
77
+ Defaults to the running loop
78
+ via `asyncio.get_running_loop()`,
79
+ or a new loop from
80
+ `asyncio.new_event_loop()` if
81
+ none is running.
79
82
  command_prefix (Union[str, List[str], Callable[['Client', Message], Union[str, List[str]]]]):
80
83
  The prefix(es) for commands. Defaults to '!'.
81
84
  verbose (bool): If True, print raw HTTP and Gateway traffic for debugging.
85
+ mention_replies (bool): Whether replies mention the author by default.
86
+ allowed_mentions (Optional[Dict[str, Any]]): Default allowed mentions for messages.
82
87
  http_options (Optional[Dict[str, Any]]): Extra options passed to
83
88
  :class:`HTTPClient` for creating the internal
84
89
  :class:`aiohttp.ClientSession`.
90
+ message_cache_maxlen (Optional[int]): Maximum number of messages to keep
91
+ in the cache. When ``None``, the cache size is unlimited.
85
92
  """
86
93
 
87
94
  def __init__(
@@ -95,10 +102,12 @@ class Client:
95
102
  application_id: Optional[Union[str, int]] = None,
96
103
  verbose: bool = False,
97
104
  mention_replies: bool = False,
105
+ allowed_mentions: Optional[Dict[str, Any]] = None,
98
106
  shard_count: Optional[int] = None,
99
107
  gateway_max_retries: int = 5,
100
108
  gateway_max_backoff: float = 60.0,
101
109
  member_cache_flags: Optional[MemberCacheFlags] = None,
110
+ message_cache_maxlen: Optional[int] = None,
102
111
  http_options: Optional[Dict[str, Any]] = None,
103
112
  ):
104
113
  if not token:
@@ -108,6 +117,7 @@ class Client:
108
117
  self.member_cache_flags: MemberCacheFlags = (
109
118
  member_cache_flags if member_cache_flags is not None else MemberCacheFlags()
110
119
  )
120
+ self.message_cache_maxlen: Optional[int] = message_cache_maxlen
111
121
  self.intents: int = intents if intents is not None else GatewayIntent.default()
112
122
  if loop:
113
123
  self.loop: asyncio.AbstractEventLoop = loop
@@ -157,7 +167,7 @@ class Client:
157
167
  self._guilds: GuildCache = GuildCache()
158
168
  self._channels: ChannelCache = ChannelCache()
159
169
  self._users: Cache["User"] = Cache()
160
- self._messages: Cache["Message"] = Cache(ttl=3600) # Cache messages for an hour
170
+ self._messages: Cache["Message"] = Cache(ttl=3600, maxlen=message_cache_maxlen)
161
171
  self._views: Dict[Snowflake, "View"] = {}
162
172
  self._persistent_views: Dict[str, "View"] = {}
163
173
  self._voice_clients: Dict[Snowflake, VoiceClient] = {}
@@ -165,6 +175,7 @@ class Client:
165
175
 
166
176
  # Default whether replies mention the user
167
177
  self.mention_replies: bool = mention_replies
178
+ self.allowed_mentions: Optional[Dict[str, Any]] = allowed_mentions
168
179
 
169
180
  # Basic signal handling for graceful shutdown
170
181
  # This might be better handled by the user's application code, but can be a nice default.
@@ -251,17 +262,16 @@ class Client:
251
262
  raise
252
263
  except DisagreementException as e: # Includes GatewayException
253
264
  print(f"Failed to connect (Attempt {attempt + 1}/{max_retries}): {e}")
254
- if attempt < max_retries - 1:
255
- print(f"Retrying in {retry_delay} seconds...")
256
- await asyncio.sleep(retry_delay)
257
- retry_delay = min(
258
- retry_delay * 2, 60
259
- ) # Exponential backoff up to 60s
260
- else:
261
- print("Max connection retries reached. Giving up.")
262
- await self.close() # Ensure cleanup
263
- raise
264
- # Should not be reached if max_retries is > 0
265
+ if attempt < max_retries - 1:
266
+ print(f"Retrying in {retry_delay} seconds...")
267
+ await asyncio.sleep(retry_delay)
268
+ retry_delay = min(
269
+ retry_delay * 2, 60
270
+ ) # Exponential backoff up to 60s
271
+ else:
272
+ print("Max connection retries reached. Giving up.")
273
+ await self.close() # Ensure cleanup
274
+ raise
265
275
  if max_retries == 0: # If max_retries was 0, means no retries attempted
266
276
  raise DisagreementException("Connection failed with 0 retries allowed.")
267
277
 
@@ -399,6 +409,12 @@ class Client:
399
409
  return self._gateway.latency
400
410
  return None
401
411
 
412
+ @property
413
+ def latency_ms(self) -> Optional[float]:
414
+ """Returns the gateway latency in milliseconds, or ``None`` if unavailable."""
415
+ latency = getattr(self._gateway, "latency_ms", None)
416
+ return round(latency, 2) if latency is not None else None
417
+
402
418
  async def wait_until_ready(self) -> None:
403
419
  """|coro|
404
420
  Waits until the client is fully connected to Discord and the initial state is processed.
@@ -435,8 +451,7 @@ class Client:
435
451
  async def change_presence(
436
452
  self,
437
453
  status: str,
438
- activity_name: Optional[str] = None,
439
- activity_type: int = 0,
454
+ activity: Optional[Activity] = None,
440
455
  since: int = 0,
441
456
  afk: bool = False,
442
457
  ):
@@ -445,8 +460,7 @@ class Client:
445
460
 
446
461
  Args:
447
462
  status (str): The new status for the client (e.g., "online", "idle", "dnd", "invisible").
448
- activity_name (Optional[str]): The name of the activity.
449
- activity_type (int): The type of the activity.
463
+ activity (Optional[Activity]): Activity instance describing what the bot is doing.
450
464
  since (int): The timestamp (in milliseconds) of when the client went idle.
451
465
  afk (bool): Whether the client is AFK.
452
466
  """
@@ -456,8 +470,7 @@ class Client:
456
470
  if self._gateway:
457
471
  await self._gateway.update_presence(
458
472
  status=status,
459
- activity_name=activity_name,
460
- activity_type=activity_type,
473
+ activity=activity,
461
474
  since=since,
462
475
  afk=afk,
463
476
  )
@@ -693,7 +706,7 @@ class Client:
693
706
  )
694
707
  # import traceback
695
708
  # traceback.print_exception(type(error.original), error.original, error.original.__traceback__)
696
-
709
+
697
710
  async def on_command_completion(self, ctx: "CommandContext") -> None:
698
711
  """
699
712
  Default command completion handler. Called when a command has successfully completed.
@@ -1010,7 +1023,7 @@ class Client:
1010
1023
  embeds (Optional[List[Embed]]): A list of embeds to send. Cannot be used with `embed`.
1011
1024
  Discord supports up to 10 embeds per message.
1012
1025
  components (Optional[List[ActionRow]]): A list of ActionRow components to include.
1013
- allowed_mentions (Optional[Dict[str, Any]]): Allowed mentions for the message.
1026
+ allowed_mentions (Optional[Dict[str, Any]]): Allowed mentions for the message. Defaults to :attr:`Client.allowed_mentions`.
1014
1027
  message_reference (Optional[Dict[str, Any]]): Message reference for replying.
1015
1028
  attachments (Optional[List[Any]]): Attachments to include with the message.
1016
1029
  files (Optional[List[Any]]): Files to upload with the message.
@@ -1057,6 +1070,9 @@ class Client:
1057
1070
  if isinstance(comp, ComponentModel)
1058
1071
  ]
1059
1072
 
1073
+ if allowed_mentions is None:
1074
+ allowed_mentions = self.allowed_mentions
1075
+
1060
1076
  message_data = await self._http.send_message(
1061
1077
  channel_id=channel_id,
1062
1078
  content=content,
@@ -1428,6 +1444,24 @@ class Client:
1428
1444
 
1429
1445
  await self._http.delete_guild_template(guild_id, template_code)
1430
1446
 
1447
+ async def fetch_widget(self, guild_id: Snowflake) -> Dict[str, Any]:
1448
+ """|coro| Fetch a guild's widget settings."""
1449
+
1450
+ if self._closed:
1451
+ raise DisagreementException("Client is closed.")
1452
+
1453
+ return await self._http.get_guild_widget(guild_id)
1454
+
1455
+ async def edit_widget(
1456
+ self, guild_id: Snowflake, payload: Dict[str, Any]
1457
+ ) -> Dict[str, Any]:
1458
+ """|coro| Edit a guild's widget settings."""
1459
+
1460
+ if self._closed:
1461
+ raise DisagreementException("Client is closed.")
1462
+
1463
+ return await self._http.edit_guild_widget(guild_id, payload)
1464
+
1431
1465
  async def fetch_scheduled_events(
1432
1466
  self, guild_id: Snowflake
1433
1467
  ) -> List["ScheduledEvent"]:
@@ -1514,35 +1548,35 @@ class Client:
1514
1548
  return [self.parse_invite(inv) for inv in data]
1515
1549
 
1516
1550
  def add_persistent_view(self, view: "View") -> None:
1517
- """
1518
- Registers a persistent view with the client.
1519
-
1520
- Persistent views have a timeout of `None` and their components must have a `custom_id`.
1521
- This allows the view to be re-instantiated across bot restarts.
1522
-
1523
- Args:
1524
- view (View): The view instance to register.
1525
-
1526
- Raises:
1527
- ValueError: If the view is not persistent (timeout is not None) or if a component's
1528
- custom_id is already registered.
1529
- """
1530
- if self.is_ready():
1531
- print(
1532
- "Warning: Adding a persistent view after the client is ready. "
1533
- "This view will only be available for interactions on this session."
1534
- )
1535
-
1536
- if view.timeout is not None:
1537
- raise ValueError("Persistent views must have a timeout of None.")
1538
-
1539
- for item in view.children:
1540
- if item.custom_id: # Ensure custom_id is not None
1541
- if item.custom_id in self._persistent_views:
1542
- raise ValueError(
1543
- f"A component with custom_id '{item.custom_id}' is already registered."
1544
- )
1545
- self._persistent_views[item.custom_id] = view
1551
+ """
1552
+ Registers a persistent view with the client.
1553
+
1554
+ Persistent views have a timeout of `None` and their components must have a `custom_id`.
1555
+ This allows the view to be re-instantiated across bot restarts.
1556
+
1557
+ Args:
1558
+ view (View): The view instance to register.
1559
+
1560
+ Raises:
1561
+ ValueError: If the view is not persistent (timeout is not None) or if a component's
1562
+ custom_id is already registered.
1563
+ """
1564
+ if self.is_ready():
1565
+ print(
1566
+ "Warning: Adding a persistent view after the client is ready. "
1567
+ "This view will only be available for interactions on this session."
1568
+ )
1569
+
1570
+ if view.timeout is not None:
1571
+ raise ValueError("Persistent views must have a timeout of None.")
1572
+
1573
+ for item in view.children:
1574
+ if item.custom_id: # Ensure custom_id is not None
1575
+ if item.custom_id in self._persistent_views:
1576
+ raise ValueError(
1577
+ f"A component with custom_id '{item.custom_id}' is already registered."
1578
+ )
1579
+ self._persistent_views[item.custom_id] = view
1546
1580
 
1547
1581
  # --- Application Command Methods ---
1548
1582
  async def process_interaction(self, interaction: Interaction) -> None: