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.
Files changed (38) hide show
  1. disagreement/__init__.py +2 -4
  2. disagreement/audio.py +25 -5
  3. disagreement/cache.py +12 -3
  4. disagreement/caching.py +15 -14
  5. disagreement/client.py +86 -52
  6. disagreement/enums.py +10 -3
  7. disagreement/error_handler.py +5 -1
  8. disagreement/errors.py +1341 -3
  9. disagreement/event_dispatcher.py +1 -3
  10. disagreement/ext/__init__.py +1 -0
  11. disagreement/ext/app_commands/__init__.py +0 -2
  12. disagreement/ext/app_commands/commands.py +0 -2
  13. disagreement/ext/app_commands/context.py +0 -2
  14. disagreement/ext/app_commands/converters.py +2 -4
  15. disagreement/ext/app_commands/decorators.py +5 -7
  16. disagreement/ext/app_commands/handler.py +1 -3
  17. disagreement/ext/app_commands/hybrid.py +0 -2
  18. disagreement/ext/commands/__init__.py +0 -2
  19. disagreement/ext/commands/cog.py +0 -2
  20. disagreement/ext/commands/converters.py +16 -5
  21. disagreement/ext/commands/core.py +52 -14
  22. disagreement/ext/commands/decorators.py +3 -7
  23. disagreement/ext/commands/errors.py +0 -2
  24. disagreement/ext/commands/help.py +0 -2
  25. disagreement/ext/commands/view.py +1 -3
  26. disagreement/gateway.py +27 -25
  27. disagreement/http.py +264 -22
  28. disagreement/interactions.py +0 -2
  29. disagreement/models.py +199 -105
  30. disagreement/shard_manager.py +0 -2
  31. disagreement/ui/view.py +2 -2
  32. disagreement/voice_client.py +20 -1
  33. {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/METADATA +32 -6
  34. disagreement-0.4.0.dist-info/RECORD +55 -0
  35. disagreement-0.3.0b1.dist-info/RECORD +0 -55
  36. {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/WHEEL +0 -0
  37. {disagreement-0.3.0b1.dist-info → disagreement-0.4.0.dist-info}/licenses/LICENSE +0 -0
  38. {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.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
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__(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
 
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__(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:
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, '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
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 `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:
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 # Import 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
 
@@ -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
- loop = asyncio.get_event_loop()
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)