disagreement 0.2.0rc1__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 (170) hide show
  1. {disagreement-0.2.0rc1/disagreement.egg-info → disagreement-0.4.0}/PKG-INFO +33 -6
  2. {disagreement-0.2.0rc1 → disagreement-0.4.0}/README.md +31 -5
  3. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/__init__.py +2 -4
  4. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/audio.py +42 -5
  5. disagreement-0.4.0/disagreement/cache.py +94 -0
  6. disagreement-0.4.0/disagreement/caching.py +121 -0
  7. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/client.py +1682 -1535
  8. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/enums.py +10 -3
  9. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/error_handler.py +5 -1
  10. disagreement-0.4.0/disagreement/errors.py +1455 -0
  11. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/event_dispatcher.py +3 -5
  12. disagreement-0.4.0/disagreement/ext/__init__.py +1 -0
  13. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/app_commands/__init__.py +0 -2
  14. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/app_commands/commands.py +0 -2
  15. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/app_commands/context.py +0 -2
  16. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/app_commands/converters.py +2 -4
  17. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/app_commands/decorators.py +5 -7
  18. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/app_commands/handler.py +1 -3
  19. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/app_commands/hybrid.py +0 -2
  20. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/commands/__init__.py +63 -61
  21. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/commands/cog.py +0 -2
  22. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/commands/converters.py +16 -5
  23. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/commands/core.py +728 -563
  24. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/commands/decorators.py +294 -219
  25. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/commands/errors.py +0 -2
  26. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/commands/help.py +0 -2
  27. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/commands/view.py +1 -3
  28. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/gateway.py +632 -586
  29. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/http.py +1362 -1041
  30. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/interactions.py +0 -2
  31. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/models.py +2682 -2263
  32. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/shard_manager.py +0 -2
  33. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ui/view.py +167 -165
  34. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/voice_client.py +263 -162
  35. {disagreement-0.2.0rc1 → disagreement-0.4.0/disagreement.egg-info}/PKG-INFO +33 -6
  36. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement.egg-info/SOURCES.txt +16 -1
  37. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement.egg-info/requires.txt +1 -0
  38. disagreement-0.4.0/docs/embeds.md +22 -0
  39. disagreement-0.4.0/docs/hybrid_context.md +14 -0
  40. disagreement-0.4.0/docs/index.md +5 -0
  41. disagreement-0.4.0/docs/introduction.md +186 -0
  42. disagreement-0.4.0/docs/mentions.md +23 -0
  43. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/presence.md +13 -2
  44. disagreement-0.4.0/docs/rate_limiter.md +14 -0
  45. disagreement-0.4.0/docs/reactions.md +62 -0
  46. disagreement-0.4.0/docs/threads.md +18 -0
  47. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/basic_bot.py +7 -2
  48. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/component_bot.py +8 -2
  49. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/context_menus.py +8 -3
  50. disagreement-0.4.0/examples/example_from_readme.py +41 -0
  51. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/extension_management.py +7 -2
  52. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/hybrid_bot.py +8 -3
  53. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/message_history.py +8 -2
  54. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/modal_command.py +8 -2
  55. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/modal_send.py +7 -2
  56. disagreement-0.4.0/examples/reactions.py +148 -0
  57. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/sharded_bot.py +8 -2
  58. disagreement-0.4.0/examples/typing_indicator.py +119 -0
  59. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/voice_bot.py +7 -2
  60. {disagreement-0.2.0rc1 → disagreement-0.4.0}/pyproject.toml +57 -56
  61. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_additional_converters.py +26 -18
  62. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_cache.py +11 -0
  63. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_channel_permissions.py +8 -8
  64. disagreement-0.4.0/tests/test_client_message_cache.py +23 -0
  65. disagreement-0.4.0/tests/test_embed_methods.py +18 -0
  66. disagreement-0.4.0/tests/test_errors.py +112 -0
  67. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_event_dispatcher.py +10 -1
  68. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_gateway_backoff.py +2 -2
  69. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_http_reactions.py +1 -3
  70. disagreement-0.4.0/tests/test_message_clean_content.py +23 -0
  71. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_presence_update.py +4 -3
  72. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_textchannel_purge.py +2 -4
  73. disagreement-0.4.0/tests/test_voice_client.py +207 -0
  74. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_webhooks.py +6 -13
  75. disagreement-0.4.0/tests/test_widget.py +50 -0
  76. disagreement-0.2.0rc1/disagreement/cache.py +0 -55
  77. disagreement-0.2.0rc1/disagreement/errors.py +0 -117
  78. disagreement-0.2.0rc1/disagreement/ext/__init__.py +0 -0
  79. disagreement-0.2.0rc1/docs/reactions.md +0 -32
  80. disagreement-0.2.0rc1/tests/test_errors.py +0 -32
  81. disagreement-0.2.0rc1/tests/test_voice_client.py +0 -106
  82. {disagreement-0.2.0rc1 → disagreement-0.4.0}/LICENSE +0 -0
  83. {disagreement-0.2.0rc1 → disagreement-0.4.0}/MANIFEST.in +0 -0
  84. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/color.py +0 -0
  85. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/components.py +0 -0
  86. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/loader.py +0 -0
  87. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ext/tasks.py +0 -0
  88. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/hybrid_context.py +0 -0
  89. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/i18n.py +0 -0
  90. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/logging_config.py +0 -0
  91. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/oauth.py +0 -0
  92. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/permissions.py +0 -0
  93. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/py.typed +0 -0
  94. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/rate_limiter.py +0 -0
  95. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/typing.py +0 -0
  96. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ui/__init__.py +0 -0
  97. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ui/button.py +0 -0
  98. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ui/item.py +0 -0
  99. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ui/modal.py +0 -0
  100. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/ui/select.py +0 -0
  101. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement/utils.py +0 -0
  102. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement.egg-info/dependency_links.txt +0 -0
  103. {disagreement-0.2.0rc1 → disagreement-0.4.0}/disagreement.egg-info/top_level.txt +0 -0
  104. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/audit_logs.md +0 -0
  105. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/caching.md +0 -0
  106. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/commands.md +0 -0
  107. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/context_menus.md +0 -0
  108. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/converters.md +0 -0
  109. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/events.md +0 -0
  110. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/extension_loader.md +0 -0
  111. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/gateway.md +0 -0
  112. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/http_client.md +0 -0
  113. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/i18n.md +0 -0
  114. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/invites.md +0 -0
  115. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/message_history.md +0 -0
  116. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/oauth2.md +0 -0
  117. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/permissions.md +0 -0
  118. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/scheduled_events.md +0 -0
  119. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/sharding.md +0 -0
  120. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/slash_commands.md +0 -0
  121. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/task_loop.md +0 -0
  122. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/typing_indicator.md +0 -0
  123. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/using_components.md +0 -0
  124. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/voice_client.md +0 -0
  125. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/voice_features.md +0 -0
  126. {disagreement-0.2.0rc1 → disagreement-0.4.0}/docs/webhooks.md +0 -0
  127. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/sample_extension.py +0 -0
  128. {disagreement-0.2.0rc1 → disagreement-0.4.0}/examples/task_loop.py +0 -0
  129. {disagreement-0.2.0rc1 → disagreement-0.4.0}/setup.cfg +0 -0
  130. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_client_context_manager.py +0 -0
  131. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_color.py +0 -0
  132. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_color_acceptance.py +0 -0
  133. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_command_checks.py +0 -0
  134. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_components_factory.py +0 -0
  135. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_context.py +0 -0
  136. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_context_menus.py +0 -0
  137. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_converter_registration.py +0 -0
  138. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_converters.py +0 -0
  139. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_error_handler.py +0 -0
  140. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_event_error_hook.py +0 -0
  141. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_extension_loader.py +0 -0
  142. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_gateway_intent.py +0 -0
  143. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_help_command.py +0 -0
  144. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_http_rate_limit.py +0 -0
  145. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_hybrid_context.py +0 -0
  146. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_i18n.py +0 -0
  147. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_interaction.py +0 -0
  148. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_logging_config.py +0 -0
  149. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_max_concurrency.py +0 -0
  150. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_member.py +0 -0
  151. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_message_pager.py +0 -0
  152. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_modal_send.py +0 -0
  153. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_modals.py +0 -0
  154. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_oauth.py +0 -0
  155. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_permissions.py +0 -0
  156. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_presence_and_typing.py +0 -0
  157. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_rate_limiter.py +0 -0
  158. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_reactions.py +0 -0
  159. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_send_files.py +0 -0
  160. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_sharding.py +0 -0
  161. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_slash_contexts.py +0 -0
  162. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_stage_instance.py +0 -0
  163. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_tasks_extension.py +0 -0
  164. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_templates.py +0 -0
  165. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_textchannel_history.py +0 -0
  166. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_typing_indicator.py +0 -0
  167. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_ui.py +0 -0
  168. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_utils.py +0 -0
  169. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_view_layout.py +0 -0
  170. {disagreement-0.2.0rc1 → disagreement-0.4.0}/tests/test_wait_for.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: disagreement
3
- Version: 0.2.0rc1
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
@@ -23,6 +23,7 @@ Requires-Python: >=3.10
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
25
  Requires-Dist: aiohttp<4.0.0,>=3.9.0
26
+ Requires-Dist: PyNaCl<2.0.0,>=1.5.0
26
27
  Provides-Extra: test
27
28
  Requires-Dist: pytest>=8.0.0; extra == "test"
28
29
  Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
@@ -37,6 +38,9 @@ A Python library for interacting with the Discord API, with a focus on bot devel
37
38
 
38
39
  ## Features
39
40
 
41
+ - Internationalization helpers
42
+ - Hybrid context for commands
43
+ - Built-in rate limiting
40
44
  - Asynchronous design using `aiohttp`
41
45
  - Gateway and HTTP API clients
42
46
  - Slash command framework
@@ -56,6 +60,13 @@ pip install -e .
56
60
 
57
61
  Requires Python 3.10 or newer.
58
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
+
59
70
  ## Basic Usage
60
71
 
61
72
  ```python
@@ -64,6 +75,8 @@ import os
64
75
 
65
76
  import disagreement
66
77
  from disagreement.ext import commands
78
+ from dotenv import load_dotenv
79
+ load_dotenv()
67
80
 
68
81
 
69
82
  class Basics(commands.Cog):
@@ -72,18 +85,17 @@ class Basics(commands.Cog):
72
85
 
73
86
  @commands.command()
74
87
  async def ping(self, ctx: commands.CommandContext) -> None:
75
- await ctx.reply("Pong!")
88
+ await ctx.reply(f"Pong! Gateway Latency: {self.client.latency_ms} ms.")
76
89
 
77
90
 
78
91
  token = os.getenv("DISCORD_BOT_TOKEN")
79
92
  if not token:
80
93
  raise RuntimeError("DISCORD_BOT_TOKEN environment variable not set")
81
94
 
82
- client = disagreement.Client(token=token, command_prefix="!")
83
- client.add_cog(Basics(client))
84
-
85
-
95
+ intents = disagreement.GatewayIntent.default() | disagreement.GatewayIntent.MESSAGE_CONTENT
96
+ client = disagreement.Client(token=token, command_prefix="!", intents=intents, mention_replies=True)
86
97
  async def main() -> None:
98
+ client.add_cog(Basics(client))
87
99
  await client.run()
88
100
 
89
101
 
@@ -135,6 +147,20 @@ These options are forwarded to ``HTTPClient`` when it creates the underlying
135
147
  ``aiohttp.ClientSession``. You can specify a custom ``connector`` or any other
136
148
  session parameter supported by ``aiohttp``.
137
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
+
138
164
  ### Defining Subcommands with `AppCommandGroup`
139
165
 
140
166
  ```python
@@ -153,6 +179,7 @@ async def show(ctx: AppCommandContext, key: str):
153
179
  @slash_command(name="set", description="Update a setting.", parent=admin_group)
154
180
  async def set_setting(ctx: AppCommandContext, key: str, value: str):
155
181
  ...
182
+ ```
156
183
  ## Fetching Guilds
157
184
 
158
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.2.0rc1"
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,
@@ -114,3 +133,21 @@ class FFmpegAudioSource(AudioSource):
114
133
  if isinstance(self.source, io.IOBase):
115
134
  with contextlib.suppress(Exception):
116
135
  self.source.close()
136
+
137
+
138
+ class AudioSink:
139
+ """Abstract base class for audio sinks."""
140
+
141
+ def write(self, user, data):
142
+ """Write a chunk of PCM audio.
143
+
144
+ Subclasses must implement this. The data is raw PCM at 48kHz
145
+ stereo.
146
+ """
147
+
148
+ raise NotImplementedError
149
+
150
+ def close(self) -> None:
151
+ """Cleanup the sink when the voice client disconnects."""
152
+
153
+ return None
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING, Dict, Generic, Optional, TypeVar
5
+ from collections import OrderedDict
6
+
7
+ if TYPE_CHECKING:
8
+ from .models import Channel, Guild, Member
9
+ from .caching import MemberCacheFlags
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ class Cache(Generic[T]):
15
+ """Simple in-memory cache with optional TTL and max size support."""
16
+
17
+ def __init__(
18
+ self, ttl: Optional[float] = None, maxlen: Optional[int] = None
19
+ ) -> None:
20
+ self.ttl = ttl
21
+ self.maxlen = maxlen
22
+ self._data: "OrderedDict[str, tuple[T, Optional[float]]]" = OrderedDict()
23
+
24
+ def set(self, key: str, value: T) -> None:
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)
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)
31
+
32
+ def get(self, key: str) -> Optional[T]:
33
+ item = self._data.get(key)
34
+ if not item:
35
+ return None
36
+ value, expiry = item
37
+ if expiry is not None and expiry < time.monotonic():
38
+ self.invalidate(key)
39
+ return None
40
+ self._data.move_to_end(key)
41
+ return value
42
+
43
+ def invalidate(self, key: str) -> None:
44
+ self._data.pop(key, None)
45
+
46
+ def clear(self) -> None:
47
+ self._data.clear()
48
+
49
+ def values(self) -> list[T]:
50
+ now = time.monotonic()
51
+ items = []
52
+ for key, (value, expiry) in list(self._data.items()):
53
+ if expiry is not None and expiry < now:
54
+ self.invalidate(key)
55
+ else:
56
+ items.append(value)
57
+ return items
58
+
59
+
60
+ class GuildCache(Cache["Guild"]):
61
+ """Cache specifically for :class:`Guild` objects."""
62
+
63
+
64
+ class ChannelCache(Cache["Channel"]):
65
+ """Cache specifically for :class:`Channel` objects."""
66
+
67
+
68
+ class MemberCache(Cache["Member"]):
69
+ """
70
+ A cache for :class:`Member` objects that respects :class:`MemberCacheFlags`.
71
+ """
72
+
73
+ def __init__(self, flags: MemberCacheFlags, ttl: Optional[float] = None) -> None:
74
+ super().__init__(ttl)
75
+ self.flags = flags
76
+
77
+ def _should_cache(self, member: Member) -> bool:
78
+ """Determines if a member should be cached based on the flags."""
79
+ if self.flags.all:
80
+ return True
81
+ if self.flags.none:
82
+ return False
83
+
84
+ if self.flags.online and member.status != "offline":
85
+ return True
86
+ if self.flags.voice and member.voice_state is not None:
87
+ return True
88
+ if self.flags.joined and getattr(member, "_just_joined", False):
89
+ return True
90
+ return False
91
+
92
+ def set(self, key: str, value: Member) -> None:
93
+ if self._should_cache(value):
94
+ super().set(key, value)
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import operator
4
+ from typing import Any, Callable, ClassVar, Dict, Iterator, Tuple
5
+
6
+
7
+ class _MemberCacheFlagValue:
8
+ flag: int
9
+
10
+ def __init__(self, func: Callable[[Any], bool]):
11
+ self.flag = getattr(func, "flag", 0)
12
+ self.__doc__ = func.__doc__
13
+
14
+ def __get__(self, instance: "MemberCacheFlags", owner: type) -> Any:
15
+ if instance is None:
16
+ return self
17
+ return instance.value & self.flag != 0
18
+
19
+ def __set__(self, instance: Any, value: bool) -> None:
20
+ if value:
21
+ instance.value |= self.flag
22
+ else:
23
+ instance.value &= ~self.flag
24
+
25
+ def __repr__(self) -> str:
26
+ return f"<{self.__class__.__name__} flag={self.flag}>"
27
+
28
+
29
+ def flag_value(flag: int) -> Callable[[Callable[[Any], bool]], _MemberCacheFlagValue]:
30
+ def decorator(func: Callable[[Any], bool]) -> _MemberCacheFlagValue:
31
+ setattr(func, "flag", flag)
32
+ return _MemberCacheFlagValue(func)
33
+
34
+ return decorator
35
+
36
+
37
+ class MemberCacheFlags:
38
+ __slots__ = ("value",)
39
+
40
+ VALID_FLAGS: ClassVar[Dict[str, int]] = {
41
+ "joined": 1 << 0,
42
+ "voice": 1 << 1,
43
+ "online": 1 << 2,
44
+ }
45
+ DEFAULT_FLAGS: ClassVar[int] = 1 | 2 | 4
46
+ ALL_FLAGS: ClassVar[int] = sum(VALID_FLAGS.values())
47
+
48
+ def __init__(self, **kwargs: bool):
49
+ self.value = self.DEFAULT_FLAGS
50
+ for key, value in kwargs.items():
51
+ if key not in self.VALID_FLAGS:
52
+ raise TypeError(f"{key!r} is not a valid member cache flag.")
53
+ setattr(self, key, value)
54
+
55
+ @classmethod
56
+ def _from_value(cls, value: int) -> MemberCacheFlags:
57
+ self = cls.__new__(cls)
58
+ self.value = value
59
+ return self
60
+
61
+ def __eq__(self, other: object) -> bool:
62
+ return isinstance(other, MemberCacheFlags) and self.value == other.value
63
+
64
+ def __ne__(self, other: object) -> bool:
65
+ return not self.__eq__(other)
66
+
67
+ def __hash__(self) -> int:
68
+ return hash(self.value)
69
+
70
+ def __repr__(self) -> str:
71
+ return f"<MemberCacheFlags value={self.value}>"
72
+
73
+ def __iter__(self) -> Iterator[Tuple[str, bool]]:
74
+ for name in self.VALID_FLAGS:
75
+ yield name, getattr(self, name)
76
+
77
+ def __int__(self) -> int:
78
+ return self.value
79
+
80
+ def __index__(self) -> int:
81
+ return self.value
82
+
83
+ @classmethod
84
+ def all(cls) -> MemberCacheFlags:
85
+ """A factory method that creates a :class:`MemberCacheFlags` with all flags enabled."""
86
+ return cls._from_value(cls.ALL_FLAGS)
87
+
88
+ @classmethod
89
+ def none(cls) -> MemberCacheFlags:
90
+ """A factory method that creates a :class:`MemberCacheFlags` with all flags disabled."""
91
+ return cls._from_value(0)
92
+
93
+ @classmethod
94
+ def only_joined(cls) -> MemberCacheFlags:
95
+ """A factory method that creates a :class:`MemberCacheFlags` with only the `joined` flag enabled."""
96
+ return cls._from_value(cls.VALID_FLAGS["joined"])
97
+
98
+ @classmethod
99
+ def only_voice(cls) -> MemberCacheFlags:
100
+ """A factory method that creates a :class:`MemberCacheFlags` with only the `voice` flag enabled."""
101
+ return cls._from_value(cls.VALID_FLAGS["voice"])
102
+
103
+ @classmethod
104
+ def only_online(cls) -> MemberCacheFlags:
105
+ """A factory method that creates a :class:`MemberCacheFlags` with only the `online` flag enabled."""
106
+ return cls._from_value(cls.VALID_FLAGS["online"])
107
+
108
+ @flag_value(1 << 0)
109
+ def joined(self) -> bool:
110
+ """Whether to cache members that have just joined the guild."""
111
+ return False
112
+
113
+ @flag_value(1 << 1)
114
+ def voice(self) -> bool:
115
+ """Whether to cache members that are in a voice channel."""
116
+ return False
117
+
118
+ @flag_value(1 << 2)
119
+ def online(self) -> bool:
120
+ """Whether to cache members that are online."""
121
+ return False