disagreement 0.3.0b1__py3-none-any.whl → 0.4.0__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 +2 -4
- disagreement/audio.py +25 -5
- disagreement/cache.py +12 -3
- disagreement/caching.py +15 -14
- disagreement/client.py +86 -52
- disagreement/enums.py +10 -3
- disagreement/error_handler.py +5 -1
- disagreement/errors.py +1341 -3
- disagreement/event_dispatcher.py +1 -3
- disagreement/ext/__init__.py +1 -0
- disagreement/ext/app_commands/__init__.py +0 -2
- disagreement/ext/app_commands/commands.py +0 -2
- disagreement/ext/app_commands/context.py +0 -2
- disagreement/ext/app_commands/converters.py +2 -4
- disagreement/ext/app_commands/decorators.py +5 -7
- disagreement/ext/app_commands/handler.py +1 -3
- disagreement/ext/app_commands/hybrid.py +0 -2
- disagreement/ext/commands/__init__.py +0 -2
- disagreement/ext/commands/cog.py +0 -2
- disagreement/ext/commands/converters.py +16 -5
- disagreement/ext/commands/core.py +52 -14
- disagreement/ext/commands/decorators.py +3 -7
- disagreement/ext/commands/errors.py +0 -2
- disagreement/ext/commands/help.py +0 -2
- disagreement/ext/commands/view.py +1 -3
- disagreement/gateway.py +27 -25
- disagreement/http.py +264 -22
- disagreement/interactions.py +0 -2
- disagreement/models.py +199 -105
- disagreement/shard_manager.py +0 -2
- disagreement/ui/view.py +2 -2
- disagreement/voice_client.py +20 -1
- {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/METADATA +32 -6
- disagreement-0.4.0.dist-info/RECORD +55 -0
- disagreement-0.3.0b1.dist-info/RECORD +0 -55
- {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/WHEEL +0 -0
- {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/top_level.txt +0 -0
disagreement/__init__.py
CHANGED
@@ -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.
|
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
|
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
|
disagreement/audio.py
CHANGED
@@ -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__(
|
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
|
-
|
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
|
-
|
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
|
|
disagreement/cache.py
CHANGED
@@ -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__(
|
17
|
+
def __init__(
|
18
|
+
self, ttl: Optional[float] = None, maxlen: Optional[int] = None
|
19
|
+
) -> None:
|
17
20
|
self.ttl = ttl
|
18
|
-
self.
|
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:
|
disagreement/caching.py
CHANGED
@@ -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,
|
11
|
+
self.flag = getattr(func, "flag", 0)
|
12
12
|
self.__doc__ = func.__doc__
|
13
13
|
|
14
|
-
def __get__(self, instance:
|
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
|
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,
|
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__ = (
|
38
|
+
__slots__ = ("value",)
|
38
39
|
|
39
40
|
VALID_FLAGS: ClassVar[Dict[str, int]] = {
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
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
|
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[
|
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[
|
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[
|
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
|
disagreement/client.py
CHANGED
@@ -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
|
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)
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1534
|
-
|
1535
|
-
|
1536
|
-
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1540
|
-
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
|
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:
|
disagreement/enums.py
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
# disagreement/enums.py
|
2
|
-
|
3
1
|
"""
|
4
2
|
Enums for Discord constants.
|
5
3
|
"""
|
6
4
|
|
7
|
-
from enum import IntEnum, Enum
|
5
|
+
from enum import IntEnum, Enum
|
8
6
|
|
9
7
|
|
10
8
|
class GatewayOpcode(IntEnum):
|
@@ -375,6 +373,15 @@ class OverwriteType(IntEnum):
|
|
375
373
|
MEMBER = 1
|
376
374
|
|
377
375
|
|
376
|
+
class AutoArchiveDuration(IntEnum):
|
377
|
+
"""Thread auto-archive duration in minutes."""
|
378
|
+
|
379
|
+
HOUR = 60
|
380
|
+
DAY = 1440
|
381
|
+
THREE_DAYS = 4320
|
382
|
+
WEEK = 10080
|
383
|
+
|
384
|
+
|
378
385
|
# --- Component Enums ---
|
379
386
|
|
380
387
|
|
disagreement/error_handler.py
CHANGED
@@ -14,7 +14,11 @@ def setup_global_error_handler(
|
|
14
14
|
The handler logs unhandled exceptions so they don't crash the bot.
|
15
15
|
"""
|
16
16
|
if loop is None:
|
17
|
-
|
17
|
+
try:
|
18
|
+
loop = asyncio.get_running_loop()
|
19
|
+
except RuntimeError:
|
20
|
+
loop = asyncio.new_event_loop()
|
21
|
+
asyncio.set_event_loop(loop)
|
18
22
|
|
19
23
|
if not logging.getLogger().hasHandlers():
|
20
24
|
setup_logging(logging.ERROR)
|