disagreement 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- disagreement/__init__.py +36 -0
- disagreement/cache.py +55 -0
- disagreement/client.py +1144 -0
- disagreement/components.py +166 -0
- disagreement/enums.py +357 -0
- disagreement/error_handler.py +33 -0
- disagreement/errors.py +112 -0
- disagreement/event_dispatcher.py +243 -0
- disagreement/gateway.py +490 -0
- disagreement/http.py +657 -0
- disagreement/hybrid_context.py +32 -0
- disagreement/i18n.py +22 -0
- disagreement/interactions.py +572 -0
- disagreement/logging_config.py +26 -0
- disagreement/models.py +1642 -0
- disagreement/oauth.py +109 -0
- disagreement/permissions.py +99 -0
- disagreement/rate_limiter.py +75 -0
- disagreement/shard_manager.py +65 -0
- disagreement/typing.py +42 -0
- disagreement/ui/__init__.py +17 -0
- disagreement/ui/button.py +99 -0
- disagreement/ui/item.py +38 -0
- disagreement/ui/modal.py +132 -0
- disagreement/ui/select.py +92 -0
- disagreement/ui/view.py +165 -0
- disagreement/voice_client.py +120 -0
- disagreement-0.0.1.dist-info/METADATA +163 -0
- disagreement-0.0.1.dist-info/RECORD +32 -0
- disagreement-0.0.1.dist-info/WHEEL +5 -0
- disagreement-0.0.1.dist-info/licenses/LICENSE +26 -0
- disagreement-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from typing import Any, Callable, Coroutine, List, Optional, TYPE_CHECKING
|
5
|
+
|
6
|
+
from .item import Item
|
7
|
+
from ..enums import ComponentType
|
8
|
+
from ..models import SelectOption
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from ..interactions import Interaction
|
12
|
+
|
13
|
+
|
14
|
+
class Select(Item):
|
15
|
+
"""Represents a select menu component in a View.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
custom_id (str): The developer-defined identifier for the select menu.
|
19
|
+
options (List[SelectOption]): The choices in the select menu.
|
20
|
+
placeholder (Optional[str]): The placeholder text that is shown if nothing is selected.
|
21
|
+
min_values (int): The minimum number of items that must be chosen.
|
22
|
+
max_values (int): The maximum number of items that can be chosen.
|
23
|
+
disabled (bool): Whether the select menu is disabled.
|
24
|
+
row (Optional[int]): The row the select menu should be placed in, from 0 to 4.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
*,
|
30
|
+
custom_id: str,
|
31
|
+
options: List[SelectOption],
|
32
|
+
placeholder: Optional[str] = None,
|
33
|
+
min_values: int = 1,
|
34
|
+
max_values: int = 1,
|
35
|
+
disabled: bool = False,
|
36
|
+
row: Optional[int] = None,
|
37
|
+
):
|
38
|
+
super().__init__(type=ComponentType.STRING_SELECT)
|
39
|
+
self.custom_id = custom_id
|
40
|
+
self.options = options
|
41
|
+
self.placeholder = placeholder
|
42
|
+
self.min_values = min_values
|
43
|
+
self.max_values = max_values
|
44
|
+
self.disabled = disabled
|
45
|
+
self._row = row
|
46
|
+
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
48
|
+
"""Converts the select menu to a dictionary that can be sent to Discord."""
|
49
|
+
payload = {
|
50
|
+
"type": ComponentType.STRING_SELECT.value,
|
51
|
+
"custom_id": self.custom_id,
|
52
|
+
"options": [option.to_dict() for option in self.options],
|
53
|
+
"disabled": self.disabled,
|
54
|
+
}
|
55
|
+
if self.placeholder:
|
56
|
+
payload["placeholder"] = self.placeholder
|
57
|
+
if self.min_values is not None:
|
58
|
+
payload["min_values"] = self.min_values
|
59
|
+
if self.max_values is not None:
|
60
|
+
payload["max_values"] = self.max_values
|
61
|
+
return payload
|
62
|
+
|
63
|
+
|
64
|
+
def select(
|
65
|
+
*,
|
66
|
+
custom_id: str,
|
67
|
+
options: List[SelectOption],
|
68
|
+
placeholder: Optional[str] = None,
|
69
|
+
min_values: int = 1,
|
70
|
+
max_values: int = 1,
|
71
|
+
disabled: bool = False,
|
72
|
+
row: Optional[int] = None,
|
73
|
+
) -> Callable[[Callable[..., Coroutine[Any, Any, Any]]], Select]:
|
74
|
+
"""A decorator to create a select menu in a View."""
|
75
|
+
|
76
|
+
def decorator(func: Callable[..., Coroutine[Any, Any, Any]]) -> Select:
|
77
|
+
if not asyncio.iscoroutinefunction(func):
|
78
|
+
raise TypeError("Select callback must be a coroutine function.")
|
79
|
+
|
80
|
+
item = Select(
|
81
|
+
custom_id=custom_id,
|
82
|
+
options=options,
|
83
|
+
placeholder=placeholder,
|
84
|
+
min_values=min_values,
|
85
|
+
max_values=max_values,
|
86
|
+
disabled=disabled,
|
87
|
+
row=row,
|
88
|
+
)
|
89
|
+
item.callback = func
|
90
|
+
return item
|
91
|
+
|
92
|
+
return decorator
|
disagreement/ui/view.py
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import uuid
|
5
|
+
from typing import Any, Callable, Coroutine, Dict, List, Optional, TYPE_CHECKING
|
6
|
+
|
7
|
+
from ..models import ActionRow
|
8
|
+
from .item import Item
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from ..client import Client
|
12
|
+
from ..interactions import Interaction
|
13
|
+
|
14
|
+
|
15
|
+
class View:
|
16
|
+
"""Represents a container for UI components that can be sent with a message.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
timeout (Optional[float]): The number of seconds to wait for an interaction before the view times out.
|
20
|
+
Defaults to 180.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, *, timeout: Optional[float] = 180.0):
|
24
|
+
self.timeout = timeout
|
25
|
+
self.id = str(uuid.uuid4())
|
26
|
+
self.__children: List[Item] = []
|
27
|
+
self.__stopped = asyncio.Event()
|
28
|
+
self._client: Optional[Client] = None
|
29
|
+
self._message_id: Optional[str] = None
|
30
|
+
|
31
|
+
for item in self.__class__.__dict__.values():
|
32
|
+
if isinstance(item, Item):
|
33
|
+
self.add_item(item)
|
34
|
+
|
35
|
+
@property
|
36
|
+
def children(self) -> List[Item]:
|
37
|
+
return self.__children
|
38
|
+
|
39
|
+
def add_item(self, item: Item):
|
40
|
+
"""Adds an item to the view."""
|
41
|
+
if not isinstance(item, Item):
|
42
|
+
raise TypeError("Only instances of 'Item' can be added to a View.")
|
43
|
+
|
44
|
+
if len(self.__children) >= 25:
|
45
|
+
raise ValueError("A view can only have a maximum of 25 components.")
|
46
|
+
|
47
|
+
item._view = self
|
48
|
+
self.__children.append(item)
|
49
|
+
|
50
|
+
@property
|
51
|
+
def message_id(self) -> Optional[str]:
|
52
|
+
return self._message_id
|
53
|
+
|
54
|
+
@message_id.setter
|
55
|
+
def message_id(self, value: str):
|
56
|
+
self._message_id = value
|
57
|
+
|
58
|
+
def to_components(self) -> List[ActionRow]:
|
59
|
+
"""Converts the view's children into a list of ActionRow components.
|
60
|
+
|
61
|
+
This retains the original, simple layout behaviour where each item is
|
62
|
+
placed in its own :class:`ActionRow` to ensure backward compatibility.
|
63
|
+
"""
|
64
|
+
|
65
|
+
rows: List[ActionRow] = []
|
66
|
+
|
67
|
+
for item in self.children:
|
68
|
+
if item.custom_id is None:
|
69
|
+
item.custom_id = (
|
70
|
+
f"{self.id}:{item.__class__.__name__}:{len(self.__children)}"
|
71
|
+
)
|
72
|
+
|
73
|
+
rows.append(ActionRow(components=[item]))
|
74
|
+
|
75
|
+
return rows
|
76
|
+
|
77
|
+
def layout_components_advanced(self) -> List[ActionRow]:
|
78
|
+
"""Group compatible components into rows following Discord rules."""
|
79
|
+
|
80
|
+
rows: List[ActionRow] = []
|
81
|
+
|
82
|
+
for item in self.children:
|
83
|
+
if item.custom_id is None:
|
84
|
+
item.custom_id = (
|
85
|
+
f"{self.id}:{item.__class__.__name__}:{len(self.__children)}"
|
86
|
+
)
|
87
|
+
|
88
|
+
target_row = item.row
|
89
|
+
if target_row is not None:
|
90
|
+
if not 0 <= target_row <= 4:
|
91
|
+
raise ValueError("Row index must be between 0 and 4.")
|
92
|
+
|
93
|
+
while len(rows) <= target_row:
|
94
|
+
if len(rows) >= 5:
|
95
|
+
raise ValueError("A view can have at most 5 action rows.")
|
96
|
+
rows.append(ActionRow())
|
97
|
+
|
98
|
+
rows[target_row].add_component(item)
|
99
|
+
continue
|
100
|
+
|
101
|
+
placed = False
|
102
|
+
for row in rows:
|
103
|
+
try:
|
104
|
+
row.add_component(item)
|
105
|
+
placed = True
|
106
|
+
break
|
107
|
+
except ValueError:
|
108
|
+
continue
|
109
|
+
|
110
|
+
if not placed:
|
111
|
+
if len(rows) >= 5:
|
112
|
+
raise ValueError("A view can have at most 5 action rows.")
|
113
|
+
new_row = ActionRow([item])
|
114
|
+
rows.append(new_row)
|
115
|
+
|
116
|
+
return rows
|
117
|
+
|
118
|
+
def to_components_payload(self) -> List[Dict[str, Any]]:
|
119
|
+
"""Converts the view's children into a list of component dictionaries
|
120
|
+
that can be sent to the Discord API."""
|
121
|
+
return [row.to_dict() for row in self.to_components()]
|
122
|
+
|
123
|
+
async def _dispatch(self, interaction: Interaction):
|
124
|
+
"""Called by the client to dispatch an interaction to the correct item."""
|
125
|
+
if self.timeout is not None:
|
126
|
+
self.__stopped.set() # Reset the timeout on each interaction
|
127
|
+
self.__stopped.clear()
|
128
|
+
|
129
|
+
if interaction.data:
|
130
|
+
custom_id = interaction.data.custom_id
|
131
|
+
for child in self.children:
|
132
|
+
if child.custom_id == custom_id:
|
133
|
+
if child.callback:
|
134
|
+
await child.callback(self, interaction)
|
135
|
+
break
|
136
|
+
|
137
|
+
async def wait(self) -> bool:
|
138
|
+
"""Waits until the view has stopped interacting."""
|
139
|
+
return await self.__stopped.wait()
|
140
|
+
|
141
|
+
def stop(self):
|
142
|
+
"""Stops the view from listening to interactions."""
|
143
|
+
if not self.__stopped.is_set():
|
144
|
+
self.__stopped.set()
|
145
|
+
|
146
|
+
async def on_timeout(self):
|
147
|
+
"""Called when the view times out."""
|
148
|
+
pass # User can override this
|
149
|
+
|
150
|
+
async def _start(self, client: Client):
|
151
|
+
"""Starts the view's internal listener."""
|
152
|
+
self._client = client
|
153
|
+
if self.timeout is not None:
|
154
|
+
asyncio.create_task(self._timeout_task())
|
155
|
+
|
156
|
+
async def _timeout_task(self):
|
157
|
+
"""The task that waits for the timeout and then stops the view."""
|
158
|
+
try:
|
159
|
+
await asyncio.wait_for(self.wait(), timeout=self.timeout)
|
160
|
+
except asyncio.TimeoutError:
|
161
|
+
self.stop()
|
162
|
+
await self.on_timeout()
|
163
|
+
if self._client and self._message_id:
|
164
|
+
# Remove the view from the client's listeners
|
165
|
+
self._client._views.pop(self._message_id, None)
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# disagreement/voice_client.py
|
2
|
+
"""Voice gateway and UDP audio client."""
|
3
|
+
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
import asyncio
|
7
|
+
import contextlib
|
8
|
+
import socket
|
9
|
+
from typing import Optional, Sequence
|
10
|
+
|
11
|
+
import aiohttp
|
12
|
+
|
13
|
+
|
14
|
+
class VoiceClient:
|
15
|
+
"""Handles the Discord voice WebSocket connection and UDP streaming."""
|
16
|
+
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
endpoint: str,
|
20
|
+
session_id: str,
|
21
|
+
token: str,
|
22
|
+
guild_id: int,
|
23
|
+
user_id: int,
|
24
|
+
*,
|
25
|
+
ws=None,
|
26
|
+
udp: Optional[socket.socket] = None,
|
27
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
28
|
+
verbose: bool = False,
|
29
|
+
) -> None:
|
30
|
+
self.endpoint = endpoint
|
31
|
+
self.session_id = session_id
|
32
|
+
self.token = token
|
33
|
+
self.guild_id = str(guild_id)
|
34
|
+
self.user_id = str(user_id)
|
35
|
+
self._ws: Optional[aiohttp.ClientWebSocketResponse] = ws
|
36
|
+
self._udp = udp
|
37
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
38
|
+
self._heartbeat_task: Optional[asyncio.Task] = None
|
39
|
+
self._heartbeat_interval: Optional[float] = None
|
40
|
+
self._loop = loop or asyncio.get_event_loop()
|
41
|
+
self.verbose = verbose
|
42
|
+
self.ssrc: Optional[int] = None
|
43
|
+
self.secret_key: Optional[Sequence[int]] = None
|
44
|
+
self._server_ip: Optional[str] = None
|
45
|
+
self._server_port: Optional[int] = None
|
46
|
+
|
47
|
+
async def connect(self) -> None:
|
48
|
+
if self._ws is None:
|
49
|
+
self._session = aiohttp.ClientSession()
|
50
|
+
self._ws = await self._session.ws_connect(self.endpoint)
|
51
|
+
|
52
|
+
hello = await self._ws.receive_json()
|
53
|
+
self._heartbeat_interval = hello["d"]["heartbeat_interval"] / 1000
|
54
|
+
self._heartbeat_task = self._loop.create_task(self._heartbeat())
|
55
|
+
|
56
|
+
await self._ws.send_json(
|
57
|
+
{
|
58
|
+
"op": 0,
|
59
|
+
"d": {
|
60
|
+
"server_id": self.guild_id,
|
61
|
+
"user_id": self.user_id,
|
62
|
+
"session_id": self.session_id,
|
63
|
+
"token": self.token,
|
64
|
+
},
|
65
|
+
}
|
66
|
+
)
|
67
|
+
|
68
|
+
ready = await self._ws.receive_json()
|
69
|
+
data = ready["d"]
|
70
|
+
self.ssrc = data["ssrc"]
|
71
|
+
self._server_ip = data["ip"]
|
72
|
+
self._server_port = data["port"]
|
73
|
+
|
74
|
+
if self._udp is None:
|
75
|
+
self._udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
76
|
+
self._udp.connect((self._server_ip, self._server_port))
|
77
|
+
|
78
|
+
await self._ws.send_json(
|
79
|
+
{
|
80
|
+
"op": 1,
|
81
|
+
"d": {
|
82
|
+
"protocol": "udp",
|
83
|
+
"data": {
|
84
|
+
"address": self._udp.getsockname()[0],
|
85
|
+
"port": self._udp.getsockname()[1],
|
86
|
+
"mode": "xsalsa20_poly1305",
|
87
|
+
},
|
88
|
+
},
|
89
|
+
}
|
90
|
+
)
|
91
|
+
|
92
|
+
session_desc = await self._ws.receive_json()
|
93
|
+
self.secret_key = session_desc["d"].get("secret_key")
|
94
|
+
|
95
|
+
async def _heartbeat(self) -> None:
|
96
|
+
assert self._ws is not None
|
97
|
+
assert self._heartbeat_interval is not None
|
98
|
+
try:
|
99
|
+
while True:
|
100
|
+
await self._ws.send_json({"op": 3, "d": int(self._loop.time() * 1000)})
|
101
|
+
await asyncio.sleep(self._heartbeat_interval)
|
102
|
+
except asyncio.CancelledError:
|
103
|
+
pass
|
104
|
+
|
105
|
+
async def send_audio_frame(self, frame: bytes) -> None:
|
106
|
+
if not self._udp:
|
107
|
+
raise RuntimeError("UDP socket not initialised")
|
108
|
+
self._udp.send(frame)
|
109
|
+
|
110
|
+
async def close(self) -> None:
|
111
|
+
if self._heartbeat_task:
|
112
|
+
self._heartbeat_task.cancel()
|
113
|
+
with contextlib.suppress(asyncio.CancelledError):
|
114
|
+
await self._heartbeat_task
|
115
|
+
if self._ws:
|
116
|
+
await self._ws.close()
|
117
|
+
if self._session:
|
118
|
+
await self._session.close()
|
119
|
+
if self._udp:
|
120
|
+
self._udp.close()
|
@@ -0,0 +1,163 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: disagreement
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: A Python library for the Discord API.
|
5
|
+
Author-email: Slipstream <me@slipstreamm.dev>
|
6
|
+
License: BSD 3-Clause
|
7
|
+
Project-URL: Homepage, https://github.com/Slipstreamm/disagreement
|
8
|
+
Project-URL: Issues, https://github.com/Slipstreamm/disagreement/issues
|
9
|
+
Keywords: discord,api,bot,async,aiohttp
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
11
|
+
Classifier: Intended Audience :: Developers
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
13
|
+
Classifier: Operating System :: OS Independent
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
20
|
+
Classifier: Topic :: Internet
|
21
|
+
Requires-Python: >=3.11
|
22
|
+
Description-Content-Type: text/markdown
|
23
|
+
License-File: LICENSE
|
24
|
+
Requires-Dist: aiohttp<4.0.0,>=3.9.0
|
25
|
+
Provides-Extra: test
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
27
|
+
Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
|
28
|
+
Requires-Dist: hypothesis>=6.89.0; extra == "test"
|
29
|
+
Provides-Extra: dev
|
30
|
+
Requires-Dist: dotenv>=0.0.5; extra == "dev"
|
31
|
+
Dynamic: license-file
|
32
|
+
|
33
|
+
# Disagreement
|
34
|
+
|
35
|
+
A Python library for interacting with the Discord API, with a focus on bot development.
|
36
|
+
|
37
|
+
## Features
|
38
|
+
|
39
|
+
- Asynchronous design using `aiohttp`
|
40
|
+
- Gateway and HTTP API clients
|
41
|
+
- Slash command framework
|
42
|
+
- Message component helpers
|
43
|
+
- Built-in caching layer
|
44
|
+
- Experimental voice support
|
45
|
+
- Helpful error handling utilities
|
46
|
+
|
47
|
+
## Installation
|
48
|
+
|
49
|
+
```bash
|
50
|
+
python -m pip install -U pip
|
51
|
+
pip install disagreement
|
52
|
+
# or install from source for development
|
53
|
+
pip install -e .
|
54
|
+
```
|
55
|
+
|
56
|
+
Requires Python 3.11 or newer.
|
57
|
+
|
58
|
+
## Basic Usage
|
59
|
+
|
60
|
+
```python
|
61
|
+
import asyncio
|
62
|
+
import os
|
63
|
+
import disagreement
|
64
|
+
|
65
|
+
# Ensure DISCORD_BOT_TOKEN is set in your environment
|
66
|
+
client = disagreement.Client(token=os.environ.get("DISCORD_BOT_TOKEN"))
|
67
|
+
|
68
|
+
@client.on_event('MESSAGE_CREATE')
|
69
|
+
async def on_message(message: disagreement.Message):
|
70
|
+
print(f"Received: {message.content} from {message.author.username}")
|
71
|
+
if message.content.lower() == '!ping':
|
72
|
+
await message.reply('Pong!')
|
73
|
+
|
74
|
+
async def main():
|
75
|
+
if not client.token:
|
76
|
+
print("Error: DISCORD_BOT_TOKEN environment variable not set.")
|
77
|
+
return
|
78
|
+
try:
|
79
|
+
async with client:
|
80
|
+
await asyncio.Future() # run until cancelled
|
81
|
+
except KeyboardInterrupt:
|
82
|
+
print("Bot shutting down...")
|
83
|
+
# Add any other specific exception handling from your library, e.g., disagreement.AuthenticationError
|
84
|
+
|
85
|
+
if __name__ == '__main__':
|
86
|
+
asyncio.run(main())
|
87
|
+
```
|
88
|
+
|
89
|
+
### Global Error Handling
|
90
|
+
|
91
|
+
To ensure unexpected errors don't crash your bot, you can enable the library's
|
92
|
+
global error handler:
|
93
|
+
|
94
|
+
```python
|
95
|
+
import disagreement
|
96
|
+
|
97
|
+
disagreement.setup_global_error_handler()
|
98
|
+
```
|
99
|
+
|
100
|
+
Call this early in your program to log unhandled exceptions instead of letting
|
101
|
+
them terminate the process.
|
102
|
+
|
103
|
+
### Configuring Logging
|
104
|
+
|
105
|
+
Use :func:`disagreement.logging_config.setup_logging` to configure logging for
|
106
|
+
your bot. The helper accepts a logging level and an optional file path.
|
107
|
+
|
108
|
+
```python
|
109
|
+
import logging
|
110
|
+
from disagreement.logging_config import setup_logging
|
111
|
+
|
112
|
+
setup_logging(logging.INFO)
|
113
|
+
# Or log to a file
|
114
|
+
setup_logging(logging.DEBUG, file="bot.log")
|
115
|
+
```
|
116
|
+
|
117
|
+
### Defining Subcommands with `AppCommandGroup`
|
118
|
+
|
119
|
+
```python
|
120
|
+
from disagreement.ext.app_commands import AppCommandGroup
|
121
|
+
|
122
|
+
settings = AppCommandGroup("settings", "Manage settings")
|
123
|
+
|
124
|
+
@settings.command(name="show")
|
125
|
+
async def show(ctx):
|
126
|
+
"""Displays a setting."""
|
127
|
+
...
|
128
|
+
|
129
|
+
@settings.group("admin", description="Admin settings")
|
130
|
+
def admin_group():
|
131
|
+
pass
|
132
|
+
|
133
|
+
@admin_group.command(name="set")
|
134
|
+
async def set_setting(ctx, key: str, value: str):
|
135
|
+
...
|
136
|
+
## Fetching Guilds
|
137
|
+
|
138
|
+
Use `Client.fetch_guild` to retrieve a guild from the Discord API if it
|
139
|
+
isn't already cached. This is useful when working with guild IDs from
|
140
|
+
outside the gateway events.
|
141
|
+
|
142
|
+
```python
|
143
|
+
guild = await client.fetch_guild("123456789012345678")
|
144
|
+
roles = await client.fetch_roles(guild.id)
|
145
|
+
```
|
146
|
+
|
147
|
+
## Sharding
|
148
|
+
|
149
|
+
To run your bot across multiple gateway shards, pass `shard_count` when creating
|
150
|
+
the client:
|
151
|
+
|
152
|
+
```python
|
153
|
+
client = disagreement.Client(token=BOT_TOKEN, shard_count=2)
|
154
|
+
```
|
155
|
+
|
156
|
+
See `examples/sharded_bot.py` for a full example.
|
157
|
+
|
158
|
+
## Contributing
|
159
|
+
|
160
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
161
|
+
|
162
|
+
See the [docs](docs/) directory for detailed guides on components, slash commands, caching, and voice features.
|
163
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
disagreement/__init__.py,sha256=ylGJLwgNmt7B71khVq2O0fG__4kjnkA35rZFX5tEDrk,907
|
2
|
+
disagreement/cache.py,sha256=juabGFl4naQih5OUIVN2aN-vAfw2ZC2cI38s4nGEn8U,1525
|
3
|
+
disagreement/client.py,sha256=pFXj7z7R1kJWz3_GWOzDYrF1S-nQ1agjcXnRTSh-PWE,45820
|
4
|
+
disagreement/components.py,sha256=W_R9iMETkQj6sr-Lzk2n7hLwLNaLWT4vBPArIPHQUNc,5232
|
5
|
+
disagreement/enums.py,sha256=LLeXdYKcx4TUhlojNV5X4NDuvscMbnteWRNW79d0C2c,9668
|
6
|
+
disagreement/error_handler.py,sha256=c2lb6aTMnhTtITQuR6axZUtEaasYKUgmdSxAHEkeq50,1028
|
7
|
+
disagreement/errors.py,sha256=rCr9jVAzK8wsS6mxieeWpffKhTDX--sHuOBz45kwsAA,3215
|
8
|
+
disagreement/event_dispatcher.py,sha256=BevGAi72qXAHN_FqCOSdvVhOhINLeI2ojyVLmvrSKJ0,9851
|
9
|
+
disagreement/gateway.py,sha256=V46WyZE1duVEvVcBYsaThWd60Xg9T2Qsqu9Y02rQwXo,21315
|
10
|
+
disagreement/http.py,sha256=1lHIEq2RRVOzzSpfj9TNGprJsMW_nhbj_8-fPr0IupI,23986
|
11
|
+
disagreement/hybrid_context.py,sha256=VYCmcreTZdPBU9v-Cy48W38vgWO2t8nM2ulC6_z4HjU,1095
|
12
|
+
disagreement/i18n.py,sha256=1L4rcFuKP0XjHk9dVwbNh4BkLk2ZlxxZ_-tecGWa9S0,718
|
13
|
+
disagreement/interactions.py,sha256=corwLVsbWM2JXHk5u8VR_Qp3GINLcKFo2y7dyI53QFA,21645
|
14
|
+
disagreement/logging_config.py,sha256=4q6baQPE6X_0lfaBTFMU1uqc03x5SbJqo2hsApdDFac,686
|
15
|
+
disagreement/models.py,sha256=TMvGP17h1LK72SlA4vNUId76LdonKAUCTzqeNJRFzVQ,61475
|
16
|
+
disagreement/oauth.py,sha256=TfDdCwg1J7osM9wDi61dtNBA5BrQk5DeQrrHsYycH34,2810
|
17
|
+
disagreement/permissions.py,sha256=7g5cIlg-evHXOL0-pmtT5EwqcB-stXot1HZSLz724sE,3008
|
18
|
+
disagreement/rate_limiter.py,sha256=ubwR_UTPs2MotipBdtqpgwQKx0IHt2I3cdfFcXTFv7g,2521
|
19
|
+
disagreement/shard_manager.py,sha256=R0HXruJ0Wda_3ftTztQx7kpI182jyuAMsjU6fDtz8Us,2039
|
20
|
+
disagreement/typing.py,sha256=_1oFWfZ4HyH5Q3bnF7CO24s79z-3_B5Qb69kWiwLhhU,1242
|
21
|
+
disagreement/voice_client.py,sha256=KdFEH8PI6v45olTYoW_-DVOuTCk8SGg7BgSzxOQsILs,3869
|
22
|
+
disagreement/ui/__init__.py,sha256=PLA6eHiq9cu7JDOKS-7MKtaFhlqswjbI4AEUlpnbgO0,307
|
23
|
+
disagreement/ui/button.py,sha256=GHbY3-yMrvv6u674-qYONocuC1e2a0flEWjPKwJXKDo,3163
|
24
|
+
disagreement/ui/item.py,sha256=bm-EmQEZpe8Kt8JrRw-o0uQdccgjErORcFsBqaXOcV8,1112
|
25
|
+
disagreement/ui/modal.py,sha256=FLWFy_VkZ9UAPumX3Q_bT0q7M06O1Q7XzBLhCZyhYhI,4120
|
26
|
+
disagreement/ui/select.py,sha256=XjWRlWkA09QZaDDLn-wDDOWIuj0Mb4VCWJEOAaExZXw,3018
|
27
|
+
disagreement/ui/view.py,sha256=QhWoYt39QKXwl1X6Mkm5gNNEqd8bt7O505lSpiG0L04,5567
|
28
|
+
disagreement-0.0.1.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
|
29
|
+
disagreement-0.0.1.dist-info/METADATA,sha256=r9liBFa4OLtdW91XF2A7yJ7VcaOiBkmRbKVG74NEsHc,4533
|
30
|
+
disagreement-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
31
|
+
disagreement-0.0.1.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
|
32
|
+
disagreement-0.0.1.dist-info/RECORD,,
|
@@ -0,0 +1,26 @@
|
|
1
|
+
Copyright (c) 2025, Slipstream
|
2
|
+
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
5
|
+
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
7
|
+
list of conditions and the following disclaimer.
|
8
|
+
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
3. Neither the name of the copyright holder nor the names of its
|
14
|
+
contributors may be used to endorse or promote products derived from
|
15
|
+
this software without specific prior written permission.
|
16
|
+
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
21
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
22
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
23
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
24
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
25
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
26
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@@ -0,0 +1 @@
|
|
1
|
+
disagreement
|