disagreement 0.1.0rc2__py3-none-any.whl → 0.1.0rc3__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 +8 -4
- disagreement/client.py +0 -4
- disagreement/ext/app_commands/handler.py +25 -12
- disagreement/ext/app_commands/hybrid.py +1 -1
- disagreement/ext/commands/cog.py +15 -6
- disagreement/ext/commands/core.py +20 -10
- disagreement/gateway.py +102 -63
- disagreement/http.py +19 -4
- disagreement/models.py +3 -6
- disagreement/py.typed +0 -0
- {disagreement-0.1.0rc2.dist-info → disagreement-0.1.0rc3.dist-info}/METADATA +1 -1
- {disagreement-0.1.0rc2.dist-info → disagreement-0.1.0rc3.dist-info}/RECORD +15 -14
- {disagreement-0.1.0rc2.dist-info → disagreement-0.1.0rc3.dist-info}/WHEEL +0 -0
- {disagreement-0.1.0rc2.dist-info → disagreement-0.1.0rc3.dist-info}/licenses/LICENSE +0 -0
- {disagreement-0.1.0rc2.dist-info → disagreement-0.1.0rc3.dist-info}/top_level.txt +0 -0
disagreement/__init__.py
CHANGED
@@ -14,7 +14,7 @@ __title__ = "disagreement"
|
|
14
14
|
__author__ = "Slipstream"
|
15
15
|
__license__ = "BSD 3-Clause License"
|
16
16
|
__copyright__ = "Copyright 2025 Slipstream"
|
17
|
-
__version__ = "0.1.
|
17
|
+
__version__ = "0.1.0rc3"
|
18
18
|
|
19
19
|
from .client import Client, AutoShardedClient
|
20
20
|
from .models import Message, User, Reaction
|
@@ -35,7 +35,11 @@ from .enums import GatewayIntent, GatewayOpcode # Export enums
|
|
35
35
|
from .error_handler import setup_global_error_handler
|
36
36
|
from .hybrid_context import HybridContext
|
37
37
|
from .ext import tasks
|
38
|
+
from .logging_config import setup_logging
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
import logging
|
41
|
+
|
42
|
+
|
43
|
+
# Configure a default logger if none has been configured yet
|
44
|
+
if not logging.getLogger().hasHandlers():
|
45
|
+
setup_logging(logging.INFO)
|
disagreement/client.py
CHANGED
@@ -123,14 +123,10 @@ class Client:
|
|
123
123
|
|
124
124
|
self._closed: bool = False
|
125
125
|
self._ready_event: asyncio.Event = asyncio.Event()
|
126
|
-
self.application_id: Optional[Snowflake] = None # For Application Commands
|
127
126
|
self.user: Optional["User"] = (
|
128
127
|
None # The bot's own user object, populated on READY
|
129
128
|
)
|
130
129
|
|
131
|
-
# Initialize AppCommandHandler
|
132
|
-
self.app_command_handler: AppCommandHandler = AppCommandHandler(client=self)
|
133
|
-
|
134
130
|
# Internal Caches
|
135
131
|
self._guilds: Dict[Snowflake, "Guild"] = {}
|
136
132
|
self._channels: Dict[Snowflake, "Channel"] = (
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# disagreement/ext/app_commands/handler.py
|
2
2
|
|
3
3
|
import inspect
|
4
|
+
import logging
|
4
5
|
from typing import (
|
5
6
|
TYPE_CHECKING,
|
6
7
|
Dict,
|
@@ -64,6 +65,9 @@ if not TYPE_CHECKING:
|
|
64
65
|
Message = Any
|
65
66
|
|
66
67
|
|
68
|
+
logger = logging.getLogger(__name__)
|
69
|
+
|
70
|
+
|
67
71
|
class AppCommandHandler:
|
68
72
|
"""
|
69
73
|
Manages application command registration, parsing, and dispatching.
|
@@ -544,7 +548,7 @@ class AppCommandHandler:
|
|
544
548
|
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
545
549
|
|
546
550
|
except Exception as e:
|
547
|
-
|
551
|
+
logger.error("Error invoking app command '%s': %s", command.name, e)
|
548
552
|
await self.dispatch_app_command_error(ctx, e)
|
549
553
|
# else:
|
550
554
|
# # Default error reply if no handler on client
|
@@ -594,34 +598,43 @@ class AppCommandHandler:
|
|
594
598
|
payload = cmd_or_group.to_dict()
|
595
599
|
commands_to_sync.append(payload)
|
596
600
|
except AttributeError:
|
597
|
-
|
598
|
-
|
601
|
+
logger.warning(
|
602
|
+
"Command or group '%s' does not have a to_dict() method. Skipping.",
|
603
|
+
cmd_or_group.name,
|
599
604
|
)
|
600
605
|
except Exception as e:
|
601
|
-
|
602
|
-
|
606
|
+
logger.error(
|
607
|
+
"Error converting command/group '%s' to dict: %s. Skipping.",
|
608
|
+
cmd_or_group.name,
|
609
|
+
e,
|
603
610
|
)
|
604
611
|
|
605
612
|
if not commands_to_sync:
|
606
|
-
|
607
|
-
|
613
|
+
logger.info(
|
614
|
+
"No commands to sync for %s scope.",
|
615
|
+
f"guild {guild_id}" if guild_id else "global",
|
608
616
|
)
|
609
617
|
return
|
610
618
|
|
611
619
|
try:
|
612
620
|
if guild_id:
|
613
|
-
|
614
|
-
|
621
|
+
logger.info(
|
622
|
+
"Syncing %s commands for guild %s...",
|
623
|
+
len(commands_to_sync),
|
624
|
+
guild_id,
|
615
625
|
)
|
616
626
|
await self.client._http.bulk_overwrite_guild_application_commands(
|
617
627
|
application_id, guild_id, commands_to_sync
|
618
628
|
)
|
619
629
|
else:
|
620
|
-
|
630
|
+
logger.info(
|
631
|
+
"Syncing %s global commands...",
|
632
|
+
len(commands_to_sync),
|
633
|
+
)
|
621
634
|
await self.client._http.bulk_overwrite_global_application_commands(
|
622
635
|
application_id, commands_to_sync
|
623
636
|
)
|
624
|
-
|
637
|
+
logger.info("Command sync successful.")
|
625
638
|
except Exception as e:
|
626
|
-
|
639
|
+
logger.error("Error syncing application commands: %s", e)
|
627
640
|
# Consider re-raising or specific error handling
|
@@ -58,4 +58,4 @@ class HybridCommand(SlashCommand, PrefixCommand): # Inherit from both
|
|
58
58
|
# The correct one will be called depending on how the command is dispatched.
|
59
59
|
# The AppCommandHandler will use AppCommand.invoke (via SlashCommand).
|
60
60
|
# The prefix CommandHandler will use PrefixCommand.invoke.
|
61
|
-
# This seems acceptable.
|
61
|
+
# This seems acceptable.
|
disagreement/ext/commands/cog.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# disagreement/ext/commands/cog.py
|
2
2
|
|
3
3
|
import inspect
|
4
|
+
import logging
|
4
5
|
from typing import TYPE_CHECKING, List, Tuple, Callable, Awaitable, Any, Dict, Union
|
5
6
|
|
6
7
|
if TYPE_CHECKING:
|
@@ -16,6 +17,8 @@ else: # pragma: no cover - runtime imports for isinstance checks
|
|
16
17
|
# EventDispatcher might be needed if cogs register listeners directly
|
17
18
|
# from disagreement.event_dispatcher import EventDispatcher
|
18
19
|
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
19
22
|
|
20
23
|
class Cog:
|
21
24
|
"""
|
@@ -59,8 +62,10 @@ class Cog:
|
|
59
62
|
cmd.cog = self # Assign the cog instance to the command
|
60
63
|
if cmd.name in self._commands:
|
61
64
|
# This should ideally be caught earlier or handled by CommandHandler
|
62
|
-
|
63
|
-
|
65
|
+
logger.warning(
|
66
|
+
"Duplicate command name '%s' in cog '%s'. Overwriting.",
|
67
|
+
cmd.name,
|
68
|
+
self.cog_name,
|
64
69
|
)
|
65
70
|
self._commands[cmd.name.lower()] = cmd
|
66
71
|
# Also register aliases
|
@@ -79,8 +84,10 @@ class Cog:
|
|
79
84
|
# For AppCommandGroup, its commands will have cog set individually if they are AppCommands
|
80
85
|
self._app_commands_and_groups.append(app_cmd_obj)
|
81
86
|
else:
|
82
|
-
|
83
|
-
|
87
|
+
logger.warning(
|
88
|
+
"Member '%s' in cog '%s' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup.",
|
89
|
+
member_name,
|
90
|
+
self.cog_name,
|
84
91
|
)
|
85
92
|
|
86
93
|
elif isinstance(member, (AppCommand, AppCommandGroup)):
|
@@ -92,8 +99,10 @@ class Cog:
|
|
92
99
|
# This is a method decorated with @commands.Cog.listener or @commands.listener
|
93
100
|
if not inspect.iscoroutinefunction(member):
|
94
101
|
# Decorator should have caught this, but double check
|
95
|
-
|
96
|
-
|
102
|
+
logger.warning(
|
103
|
+
"Listener '%s' in cog '%s' is not a coroutine. Skipping.",
|
104
|
+
member_name,
|
105
|
+
self.cog_name,
|
97
106
|
)
|
98
107
|
continue
|
99
108
|
|
@@ -3,6 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
+
import logging
|
6
7
|
import inspect
|
7
8
|
from typing import (
|
8
9
|
TYPE_CHECKING,
|
@@ -31,6 +32,8 @@ from .errors import (
|
|
31
32
|
from .converters import run_converters, DEFAULT_CONVERTERS, Converter
|
32
33
|
from disagreement.typing import Typing
|
33
34
|
|
35
|
+
logger = logging.getLogger(__name__)
|
36
|
+
|
34
37
|
if TYPE_CHECKING:
|
35
38
|
from .cog import Cog
|
36
39
|
from disagreement.client import Client
|
@@ -224,8 +227,10 @@ class CommandHandler:
|
|
224
227
|
self.commands[command.name.lower()] = command
|
225
228
|
for alias in command.aliases:
|
226
229
|
if alias in self.commands:
|
227
|
-
|
228
|
-
|
230
|
+
logger.warning(
|
231
|
+
"Alias '%s' for command '%s' conflicts with an existing command or alias.",
|
232
|
+
alias,
|
233
|
+
command.name,
|
229
234
|
)
|
230
235
|
self.commands[alias.lower()] = command
|
231
236
|
|
@@ -241,6 +246,7 @@ class CommandHandler:
|
|
241
246
|
|
242
247
|
def add_cog(self, cog_to_add: "Cog") -> None:
|
243
248
|
from .cog import Cog
|
249
|
+
|
244
250
|
if not isinstance(cog_to_add, Cog):
|
245
251
|
raise TypeError("Argument must be a subclass of Cog.")
|
246
252
|
|
@@ -258,8 +264,9 @@ class CommandHandler:
|
|
258
264
|
for event_name, callback in cog_to_add.get_listeners():
|
259
265
|
self.client._event_dispatcher.register(event_name.upper(), callback)
|
260
266
|
else:
|
261
|
-
|
262
|
-
|
267
|
+
logger.warning(
|
268
|
+
"Client does not have '_event_dispatcher'. Listeners for cog '%s' not registered.",
|
269
|
+
cog_to_add.cog_name,
|
263
270
|
)
|
264
271
|
|
265
272
|
if hasattr(cog_to_add, "cog_load") and inspect.iscoroutinefunction(
|
@@ -267,7 +274,7 @@ class CommandHandler:
|
|
267
274
|
):
|
268
275
|
asyncio.create_task(cog_to_add.cog_load())
|
269
276
|
|
270
|
-
|
277
|
+
logger.info("Cog '%s' added.", cog_to_add.cog_name)
|
271
278
|
|
272
279
|
def remove_cog(self, cog_name: str) -> Optional["Cog"]:
|
273
280
|
cog_to_remove = self.cogs.pop(cog_name, None)
|
@@ -277,8 +284,11 @@ class CommandHandler:
|
|
277
284
|
|
278
285
|
if hasattr(self.client, "_event_dispatcher"):
|
279
286
|
for event_name, callback in cog_to_remove.get_listeners():
|
280
|
-
|
281
|
-
|
287
|
+
logger.debug(
|
288
|
+
"Listener '%s' for event '%s' from cog '%s' needs manual unregistration logic in EventDispatcher.",
|
289
|
+
callback.__name__,
|
290
|
+
event_name,
|
291
|
+
cog_name,
|
282
292
|
)
|
283
293
|
|
284
294
|
if hasattr(cog_to_remove, "cog_unload") and inspect.iscoroutinefunction(
|
@@ -287,7 +297,7 @@ class CommandHandler:
|
|
287
297
|
asyncio.create_task(cog_to_remove.cog_unload())
|
288
298
|
|
289
299
|
cog_to_remove._eject()
|
290
|
-
|
300
|
+
logger.info("Cog '%s' removed.", cog_name)
|
291
301
|
return cog_to_remove
|
292
302
|
|
293
303
|
async def get_prefix(self, message: "Message") -> Union[str, List[str], None]:
|
@@ -493,11 +503,11 @@ class CommandHandler:
|
|
493
503
|
ctx.kwargs = parsed_kwargs
|
494
504
|
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
495
505
|
except CommandError as e:
|
496
|
-
|
506
|
+
logger.error("Command error for '%s': %s", command.name, e)
|
497
507
|
if hasattr(self.client, "on_command_error"):
|
498
508
|
await self.client.on_command_error(ctx, e)
|
499
509
|
except Exception as e:
|
500
|
-
|
510
|
+
logger.error("Unexpected error invoking command '%s': %s", command.name, e)
|
501
511
|
exc = CommandInvokeError(e)
|
502
512
|
if hasattr(self.client, "on_command_error"):
|
503
513
|
await self.client.on_command_error(ctx, exc)
|
disagreement/gateway.py
CHANGED
@@ -5,6 +5,7 @@ Manages the WebSocket connection to the Discord Gateway.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import asyncio
|
8
|
+
import logging
|
8
9
|
import traceback
|
9
10
|
import aiohttp
|
10
11
|
import json
|
@@ -28,6 +29,9 @@ ZLIB_SUFFIX = b"\x00\x00\xff\xff"
|
|
28
29
|
MAX_DECOMPRESSION_SIZE = 10 * 1024 * 1024 # 10 MiB, adjust as needed
|
29
30
|
|
30
31
|
|
32
|
+
logger = logging.getLogger(__name__)
|
33
|
+
|
34
|
+
|
31
35
|
class GatewayClient:
|
32
36
|
"""
|
33
37
|
Handles the Discord Gateway WebSocket connection, heartbeating, and event dispatching.
|
@@ -84,13 +88,17 @@ class GatewayClient:
|
|
84
88
|
return
|
85
89
|
except Exception as e: # noqa: BLE001
|
86
90
|
if attempt >= self._max_retries - 1:
|
87
|
-
|
91
|
+
logger.error(
|
92
|
+
"Reconnect failed after %s attempts: %s", attempt + 1, e
|
93
|
+
)
|
88
94
|
raise
|
89
95
|
jitter = random.uniform(0, delay)
|
90
96
|
wait_time = min(delay + jitter, self._max_backoff)
|
91
|
-
|
92
|
-
|
93
|
-
|
97
|
+
logger.warning(
|
98
|
+
"Reconnect attempt %s failed: %s. Retrying in %.2f seconds...",
|
99
|
+
attempt + 1,
|
100
|
+
e,
|
101
|
+
wait_time,
|
94
102
|
)
|
95
103
|
await asyncio.sleep(wait_time)
|
96
104
|
delay = min(delay * 2, self._max_backoff)
|
@@ -112,21 +120,23 @@ class GatewayClient:
|
|
112
120
|
self._buffer.clear() # Reset buffer after successful decompression
|
113
121
|
return json.loads(decompressed.decode("utf-8"))
|
114
122
|
except zlib.error as e:
|
115
|
-
|
123
|
+
logger.error("Zlib decompression error: %s", e)
|
116
124
|
self._buffer.clear() # Clear buffer on error
|
117
125
|
self._inflator = zlib.decompressobj() # Reset inflator
|
118
126
|
return None
|
119
127
|
except json.JSONDecodeError as e:
|
120
|
-
|
128
|
+
logger.error("JSON decode error after decompression: %s", e)
|
121
129
|
return None
|
122
130
|
|
123
131
|
async def _send_json(self, payload: Dict[str, Any]):
|
124
132
|
if self._ws and not self._ws.closed:
|
125
133
|
if self.verbose:
|
126
|
-
|
134
|
+
logger.debug("GATEWAY SEND: %s", payload)
|
127
135
|
await self._ws.send_json(payload)
|
128
136
|
else:
|
129
|
-
|
137
|
+
logger.warning(
|
138
|
+
"Gateway send attempted but WebSocket is closed or not available."
|
139
|
+
)
|
130
140
|
# raise GatewayException("WebSocket is not connected.")
|
131
141
|
|
132
142
|
async def _heartbeat(self):
|
@@ -140,7 +150,7 @@ class GatewayClient:
|
|
140
150
|
"""Manages the heartbeating loop."""
|
141
151
|
if self._heartbeat_interval is None:
|
142
152
|
# This should not happen if HELLO was processed correctly
|
143
|
-
|
153
|
+
logger.error("Heartbeat interval not set. Cannot start keep_alive.")
|
144
154
|
return
|
145
155
|
|
146
156
|
try:
|
@@ -150,9 +160,9 @@ class GatewayClient:
|
|
150
160
|
self._heartbeat_interval / 1000
|
151
161
|
) # Interval is in ms
|
152
162
|
except asyncio.CancelledError:
|
153
|
-
|
163
|
+
logger.debug("Keep_alive task cancelled.")
|
154
164
|
except Exception as e:
|
155
|
-
|
165
|
+
logger.error("Error in keep_alive loop: %s", e)
|
156
166
|
# Potentially trigger a reconnect here or notify client
|
157
167
|
await self._client_instance.close_gateway(code=1000) # Generic close
|
158
168
|
|
@@ -174,12 +184,12 @@ class GatewayClient:
|
|
174
184
|
if self._shard_id is not None and self._shard_count is not None:
|
175
185
|
payload["d"]["shard"] = [self._shard_id, self._shard_count]
|
176
186
|
await self._send_json(payload)
|
177
|
-
|
187
|
+
logger.info("Sent IDENTIFY.")
|
178
188
|
|
179
189
|
async def _resume(self):
|
180
190
|
"""Sends the RESUME payload to the Gateway."""
|
181
191
|
if not self._session_id or self._last_sequence is None:
|
182
|
-
|
192
|
+
logger.warning("Cannot RESUME: session_id or last_sequence is missing.")
|
183
193
|
await self._identify() # Fallback to identify
|
184
194
|
return
|
185
195
|
|
@@ -192,8 +202,10 @@ class GatewayClient:
|
|
192
202
|
},
|
193
203
|
}
|
194
204
|
await self._send_json(payload)
|
195
|
-
|
196
|
-
|
205
|
+
logger.info(
|
206
|
+
"Sent RESUME for session %s at sequence %s.",
|
207
|
+
self._session_id,
|
208
|
+
self._last_sequence,
|
197
209
|
)
|
198
210
|
|
199
211
|
async def update_presence(
|
@@ -238,8 +250,9 @@ class GatewayClient:
|
|
238
250
|
|
239
251
|
if event_name == "READY": # Special handling for READY
|
240
252
|
if not isinstance(raw_event_d_payload, dict):
|
241
|
-
|
242
|
-
|
253
|
+
logger.error(
|
254
|
+
"READY event 'd' payload is not a dict or is missing: %s",
|
255
|
+
raw_event_d_payload,
|
243
256
|
)
|
244
257
|
# Consider raising an error or attempting a reconnect
|
245
258
|
return
|
@@ -259,8 +272,8 @@ class GatewayClient:
|
|
259
272
|
)
|
260
273
|
app_id_str = str(app_id_value)
|
261
274
|
else:
|
262
|
-
|
263
|
-
|
275
|
+
logger.warning(
|
276
|
+
"Could not find application ID in READY payload. App commands may not work."
|
264
277
|
)
|
265
278
|
|
266
279
|
# Parse and store the bot's own user object
|
@@ -274,20 +287,29 @@ class GatewayClient:
|
|
274
287
|
raw_event_d_payload["user"]
|
275
288
|
)
|
276
289
|
self._client_instance.user = bot_user_obj
|
277
|
-
|
278
|
-
|
290
|
+
logger.info(
|
291
|
+
"Gateway READY. Bot User: %s#%s. Session ID: %s. App ID: %s. Resume URL: %s",
|
292
|
+
bot_user_obj.username,
|
293
|
+
bot_user_obj.discriminator,
|
294
|
+
self._session_id,
|
295
|
+
app_id_str,
|
296
|
+
self._resume_gateway_url,
|
279
297
|
)
|
280
298
|
except Exception as e:
|
281
|
-
|
282
|
-
|
283
|
-
|
299
|
+
logger.error("Error parsing bot user from READY payload: %s", e)
|
300
|
+
logger.info(
|
301
|
+
"Gateway READY (user parse failed). Session ID: %s. App ID: %s. Resume URL: %s",
|
302
|
+
self._session_id,
|
303
|
+
app_id_str,
|
304
|
+
self._resume_gateway_url,
|
284
305
|
)
|
285
306
|
else:
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
307
|
+
logger.warning("Bot user object not found or invalid in READY payload.")
|
308
|
+
logger.info(
|
309
|
+
"Gateway READY (no user). Session ID: %s. App ID: %s. Resume URL: %s",
|
310
|
+
self._session_id,
|
311
|
+
app_id_str,
|
312
|
+
self._resume_gateway_url,
|
291
313
|
)
|
292
314
|
|
293
315
|
await self._dispatcher.dispatch(event_name, raw_event_d_payload)
|
@@ -306,15 +328,16 @@ class GatewayClient:
|
|
306
328
|
self._client_instance.process_interaction(interaction)
|
307
329
|
) # type: ignore
|
308
330
|
else:
|
309
|
-
|
310
|
-
"
|
331
|
+
logger.warning(
|
332
|
+
"Client instance does not have process_interaction method for INTERACTION_CREATE."
|
311
333
|
)
|
312
334
|
else:
|
313
|
-
|
314
|
-
|
335
|
+
logger.error(
|
336
|
+
"INTERACTION_CREATE event 'd' payload is not a dict: %s",
|
337
|
+
raw_event_d_payload,
|
315
338
|
)
|
316
339
|
elif event_name == "RESUMED":
|
317
|
-
|
340
|
+
logger.info("Gateway RESUMED successfully.")
|
318
341
|
# RESUMED 'd' payload is often an empty object or debug info.
|
319
342
|
# Ensure it's a dict for the dispatcher.
|
320
343
|
event_data_to_dispatch = (
|
@@ -330,7 +353,7 @@ class GatewayClient:
|
|
330
353
|
# print(f"GATEWAY RECV EVENT: {event_name} | DATA: {event_data_to_dispatch}")
|
331
354
|
await self._dispatcher.dispatch(event_name, event_data_to_dispatch)
|
332
355
|
else:
|
333
|
-
|
356
|
+
logger.warning("Received dispatch with no event name: %s", data)
|
334
357
|
|
335
358
|
async def _process_message(self, msg: aiohttp.WSMessage):
|
336
359
|
"""Processes a single message from the WebSocket."""
|
@@ -338,19 +361,20 @@ class GatewayClient:
|
|
338
361
|
try:
|
339
362
|
data = json.loads(msg.data)
|
340
363
|
except json.JSONDecodeError:
|
341
|
-
|
342
|
-
f"Failed to decode JSON from Gateway: {msg.data[:200]}"
|
343
|
-
) # Log snippet
|
364
|
+
logger.error("Failed to decode JSON from Gateway: %s", msg.data[:200])
|
344
365
|
return
|
345
366
|
elif msg.type == aiohttp.WSMsgType.BINARY:
|
346
367
|
decompressed_data = await self._decompress_message(msg.data)
|
347
368
|
if decompressed_data is None:
|
348
|
-
|
369
|
+
logger.error(
|
370
|
+
"Failed to decompress or decode binary message from Gateway."
|
371
|
+
)
|
349
372
|
return
|
350
373
|
data = decompressed_data
|
351
374
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
352
|
-
|
353
|
-
|
375
|
+
logger.error(
|
376
|
+
"WebSocket error: %s",
|
377
|
+
self._ws.exception() if self._ws else "Unknown WSError",
|
354
378
|
)
|
355
379
|
raise GatewayException(
|
356
380
|
f"WebSocket error: {self._ws.exception() if self._ws else 'Unknown WSError'}"
|
@@ -361,15 +385,17 @@ class GatewayClient:
|
|
361
385
|
if self._ws and hasattr(self._ws, "close_code")
|
362
386
|
else "N/A"
|
363
387
|
)
|
364
|
-
|
388
|
+
logger.warning(
|
389
|
+
"WebSocket connection closed by server. Code: %s", close_code
|
390
|
+
)
|
365
391
|
# Raise an exception to signal the closure to the client's main run loop
|
366
392
|
raise GatewayException(f"WebSocket closed by server. Code: {close_code}")
|
367
393
|
else:
|
368
|
-
|
394
|
+
logger.warning("Received unhandled WebSocket message type: %s", msg.type)
|
369
395
|
return
|
370
396
|
|
371
397
|
if self.verbose:
|
372
|
-
|
398
|
+
logger.debug("GATEWAY RECV: %s", data)
|
373
399
|
op = data.get("op")
|
374
400
|
# 'd' payload (event_data) is handled specifically by each opcode handler below
|
375
401
|
|
@@ -378,12 +404,16 @@ class GatewayClient:
|
|
378
404
|
elif op == GatewayOpcode.HEARTBEAT: # Server requests a heartbeat
|
379
405
|
await self._heartbeat()
|
380
406
|
elif op == GatewayOpcode.RECONNECT: # Server requests a reconnect
|
381
|
-
|
407
|
+
logger.info(
|
408
|
+
"Gateway requested RECONNECT. Closing and will attempt to reconnect."
|
409
|
+
)
|
382
410
|
await self.close(code=4000, reconnect=True)
|
383
411
|
elif op == GatewayOpcode.INVALID_SESSION:
|
384
412
|
# The 'd' payload for INVALID_SESSION is a boolean indicating resumability
|
385
413
|
can_resume = data.get("d") is True
|
386
|
-
|
414
|
+
logger.warning(
|
415
|
+
"Gateway indicated INVALID_SESSION. Resumable: %s", can_resume
|
416
|
+
)
|
387
417
|
if not can_resume:
|
388
418
|
self._session_id = None # Clear session_id to force re-identify
|
389
419
|
self._last_sequence = None
|
@@ -395,13 +425,16 @@ class GatewayClient:
|
|
395
425
|
not isinstance(hello_d_payload, dict)
|
396
426
|
or "heartbeat_interval" not in hello_d_payload
|
397
427
|
):
|
398
|
-
|
399
|
-
|
428
|
+
logger.error(
|
429
|
+
"HELLO event 'd' payload is invalid or missing heartbeat_interval: %s",
|
430
|
+
hello_d_payload,
|
400
431
|
)
|
401
432
|
await self.close(code=1011) # Internal error, malformed HELLO
|
402
433
|
return
|
403
434
|
self._heartbeat_interval = hello_d_payload["heartbeat_interval"]
|
404
|
-
|
435
|
+
logger.info(
|
436
|
+
"Gateway HELLO. Heartbeat interval: %sms.", self._heartbeat_interval
|
437
|
+
)
|
405
438
|
# Start heartbeating
|
406
439
|
if self._keep_alive_task:
|
407
440
|
self._keep_alive_task.cancel()
|
@@ -409,45 +442,51 @@ class GatewayClient:
|
|
409
442
|
|
410
443
|
# Identify or Resume
|
411
444
|
if self._session_id and self._resume_gateway_url: # Check if we can resume
|
412
|
-
|
445
|
+
logger.info("Attempting to RESUME session.")
|
413
446
|
await self._resume()
|
414
447
|
else:
|
415
|
-
|
448
|
+
logger.info("Performing initial IDENTIFY.")
|
416
449
|
await self._identify()
|
417
450
|
elif op == GatewayOpcode.HEARTBEAT_ACK:
|
418
451
|
self._last_heartbeat_ack = time.monotonic()
|
419
452
|
# print("Received heartbeat ACK.")
|
420
453
|
pass # Good, connection is alive
|
421
454
|
else:
|
422
|
-
|
455
|
+
logger.warning(
|
456
|
+
"Received unhandled Gateway Opcode: %s with data: %s", op, data
|
457
|
+
)
|
423
458
|
|
424
459
|
async def _receive_loop(self):
|
425
460
|
"""Continuously receives and processes messages from the WebSocket."""
|
426
461
|
if not self._ws or self._ws.closed:
|
427
|
-
|
462
|
+
logger.warning(
|
463
|
+
"Receive loop cannot start: WebSocket is not connected or closed."
|
464
|
+
)
|
428
465
|
return
|
429
466
|
|
430
467
|
try:
|
431
468
|
async for msg in self._ws:
|
432
469
|
await self._process_message(msg)
|
433
470
|
except asyncio.CancelledError:
|
434
|
-
|
471
|
+
logger.debug("Receive_loop task cancelled.")
|
435
472
|
except aiohttp.ClientConnectionError as e:
|
436
|
-
|
473
|
+
logger.warning(
|
474
|
+
"ClientConnectionError in receive_loop: %s. Attempting reconnect.", e
|
475
|
+
)
|
437
476
|
await self.close(code=1006, reconnect=True) # Abnormal closure
|
438
477
|
except Exception as e:
|
439
|
-
|
478
|
+
logger.error("Unexpected error in receive_loop: %s", e)
|
440
479
|
traceback.print_exc()
|
441
480
|
await self.close(code=1011, reconnect=True)
|
442
481
|
finally:
|
443
|
-
|
482
|
+
logger.info("Receive_loop ended.")
|
444
483
|
# If the loop ends unexpectedly (not due to explicit close),
|
445
484
|
# the main client might want to try reconnecting.
|
446
485
|
|
447
486
|
async def connect(self):
|
448
487
|
"""Connects to the Discord Gateway."""
|
449
488
|
if self._ws and not self._ws.closed:
|
450
|
-
|
489
|
+
logger.warning("Gateway already connected or connecting.")
|
451
490
|
return
|
452
491
|
|
453
492
|
gateway_url = (
|
@@ -456,14 +495,14 @@ class GatewayClient:
|
|
456
495
|
if not gateway_url.endswith("?v=10&encoding=json&compress=zlib-stream"):
|
457
496
|
gateway_url += "?v=10&encoding=json&compress=zlib-stream"
|
458
497
|
|
459
|
-
|
498
|
+
logger.info("Connecting to Gateway: %s", gateway_url)
|
460
499
|
try:
|
461
500
|
await self._http._ensure_session() # Ensure the HTTP client's session is active
|
462
501
|
assert (
|
463
502
|
self._http._session is not None
|
464
503
|
), "HTTPClient session not initialized after ensure_session"
|
465
504
|
self._ws = await self._http._session.ws_connect(gateway_url, max_msg_size=0)
|
466
|
-
|
505
|
+
logger.info("Gateway WebSocket connection established.")
|
467
506
|
|
468
507
|
if self._receive_task:
|
469
508
|
self._receive_task.cancel()
|
@@ -488,7 +527,7 @@ class GatewayClient:
|
|
488
527
|
|
489
528
|
async def close(self, code: int = 1000, *, reconnect: bool = False):
|
490
529
|
"""Closes the Gateway connection."""
|
491
|
-
|
530
|
+
logger.info("Closing Gateway connection with code %s...", code)
|
492
531
|
if self._keep_alive_task and not self._keep_alive_task.done():
|
493
532
|
self._keep_alive_task.cancel()
|
494
533
|
try:
|
@@ -507,7 +546,7 @@ class GatewayClient:
|
|
507
546
|
|
508
547
|
if self._ws and not self._ws.closed:
|
509
548
|
await self._ws.close(code=code)
|
510
|
-
|
549
|
+
logger.info("Gateway WebSocket closed.")
|
511
550
|
|
512
551
|
self._ws = None
|
513
552
|
# Do not reset session_id, last_sequence, or resume_gateway_url here
|
@@ -515,7 +554,7 @@ class GatewayClient:
|
|
515
554
|
# The connect logic will decide whether to resume or re-identify.
|
516
555
|
# However, if it's a non-resumable close (e.g. Invalid Session non-resumable), clear them.
|
517
556
|
if code == 4009: # Invalid session, not resumable
|
518
|
-
|
557
|
+
logger.info("Clearing session state due to non-resumable invalid session.")
|
519
558
|
self._session_id = None
|
520
559
|
self._last_sequence = None
|
521
560
|
self._resume_gateway_url = None # This might be re-fetched anyway
|
disagreement/http.py
CHANGED
@@ -5,6 +5,7 @@ HTTP client for interacting with the Discord REST API.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import asyncio
|
8
|
+
import logging
|
8
9
|
import aiohttp # pylint: disable=import-error
|
9
10
|
import json
|
10
11
|
from urllib.parse import quote
|
@@ -28,6 +29,8 @@ if TYPE_CHECKING:
|
|
28
29
|
# Discord API constants
|
29
30
|
API_BASE_URL = "https://discord.com/api/v10" # Using API v10
|
30
31
|
|
32
|
+
logger = logging.getLogger(__name__)
|
33
|
+
|
31
34
|
|
32
35
|
class HTTPClient:
|
33
36
|
"""Handles HTTP requests to the Discord API."""
|
@@ -86,7 +89,13 @@ class HTTPClient:
|
|
86
89
|
final_headers.update(custom_headers)
|
87
90
|
|
88
91
|
if self.verbose:
|
89
|
-
|
92
|
+
logger.debug(
|
93
|
+
"HTTP REQUEST: %s %s | payload=%s params=%s",
|
94
|
+
method,
|
95
|
+
url,
|
96
|
+
payload,
|
97
|
+
params,
|
98
|
+
)
|
90
99
|
|
91
100
|
route = f"{method.upper()}:{endpoint}"
|
92
101
|
|
@@ -119,7 +128,9 @@ class HTTPClient:
|
|
119
128
|
) # Fallback to text if JSON parsing fails
|
120
129
|
|
121
130
|
if self.verbose:
|
122
|
-
|
131
|
+
logger.debug(
|
132
|
+
"HTTP RESPONSE: %s %s | %s", response.status, url, data
|
133
|
+
)
|
123
134
|
|
124
135
|
self._rate_limiter.release(route, response.headers)
|
125
136
|
|
@@ -150,8 +161,12 @@ class HTTPClient:
|
|
150
161
|
)
|
151
162
|
|
152
163
|
if attempt < 4: # Don't log on the last attempt before raising
|
153
|
-
|
154
|
-
|
164
|
+
logger.warning(
|
165
|
+
"%s Retrying after %ss (Attempt %s/5). Global: %s",
|
166
|
+
error_message,
|
167
|
+
retry_after,
|
168
|
+
attempt + 1,
|
169
|
+
is_global,
|
155
170
|
)
|
156
171
|
continue # Retry the request
|
157
172
|
else: # Last attempt failed
|
disagreement/models.py
CHANGED
@@ -4,12 +4,12 @@
|
|
4
4
|
Data models for Discord objects.
|
5
5
|
"""
|
6
6
|
|
7
|
-
import json
|
8
|
-
import asyncio
|
9
|
-
import aiohttp # pylint: disable=import-error
|
10
7
|
import asyncio
|
8
|
+
import json
|
11
9
|
from typing import Any, AsyncIterator, Dict, List, Optional, TYPE_CHECKING, Union
|
12
10
|
|
11
|
+
import aiohttp # pylint: disable=import-error
|
12
|
+
from .color import Color
|
13
13
|
from .errors import DisagreementException, HTTPException
|
14
14
|
from .enums import ( # These enums will need to be defined in disagreement/enums.py
|
15
15
|
VerificationLevel,
|
@@ -25,7 +25,6 @@ from .enums import ( # These enums will need to be defined in disagreement/enum
|
|
25
25
|
# SelectMenuType will be part of ComponentType or a new enum if needed
|
26
26
|
)
|
27
27
|
from .permissions import Permissions
|
28
|
-
from .color import Color
|
29
28
|
|
30
29
|
|
31
30
|
if TYPE_CHECKING:
|
@@ -1087,7 +1086,6 @@ class TextChannel(Channel):
|
|
1087
1086
|
)
|
1088
1087
|
self.last_pin_timestamp: Optional[str] = data.get("last_pin_timestamp")
|
1089
1088
|
|
1090
|
-
|
1091
1089
|
def history(
|
1092
1090
|
self,
|
1093
1091
|
*,
|
@@ -1143,7 +1141,6 @@ class TextChannel(Channel):
|
|
1143
1141
|
self._client._messages.pop(mid, None)
|
1144
1142
|
return ids
|
1145
1143
|
|
1146
|
-
|
1147
1144
|
def __repr__(self) -> str:
|
1148
1145
|
return f"<TextChannel id='{self.id}' name='{self.name}' guild_id='{self.guild_id}'>"
|
1149
1146
|
|
disagreement/py.typed
ADDED
File without changes
|
@@ -1,22 +1,23 @@
|
|
1
|
-
disagreement/__init__.py,sha256=
|
1
|
+
disagreement/__init__.py,sha256=FAY6OvLuMyjkrBWEzl3tFemLgyWR-AUL-my3oeN-HB0,1169
|
2
2
|
disagreement/audio.py,sha256=P6inobI8CNhNVkaRKU58RMYtLq1RrSREioF0Mui5VlA,3351
|
3
3
|
disagreement/cache.py,sha256=juabGFl4naQih5OUIVN2aN-vAfw2ZC2cI38s4nGEn8U,1525
|
4
|
-
disagreement/client.py,sha256=
|
4
|
+
disagreement/client.py,sha256=ig5ZBCIPnAYxShhQzW9qPSRkauGnNo16DO4RzPCHLo8,52973
|
5
5
|
disagreement/color.py,sha256=g-1ynMGCUbY0f6jJXzMLS1aJFoZg91bdMetFkZgaCC0,2387
|
6
6
|
disagreement/components.py,sha256=tEYJ2RHVpIFtZuPPxZ0v8ssUw_x7ybhYBzHNsRiXXvU,5250
|
7
7
|
disagreement/enums.py,sha256=CP03oF28maaPUVckOfE_tnVIm2ZOc5WL8mWlocmjeOQ,9785
|
8
8
|
disagreement/error_handler.py,sha256=c2lb6aTMnhTtITQuR6axZUtEaasYKUgmdSxAHEkeq50,1028
|
9
9
|
disagreement/errors.py,sha256=XiYVPy8uFUGVi_EIf81yK7QbC7KyN4JHplSJSWw2RRk,3185
|
10
10
|
disagreement/event_dispatcher.py,sha256=mp4LVhIj0SW1P2NruqbYpZoYH33X5rXvkAl3-RK40kE,11460
|
11
|
-
disagreement/gateway.py,sha256=
|
12
|
-
disagreement/http.py,sha256=
|
11
|
+
disagreement/gateway.py,sha256=EnRQKdYzeQ08h_Rx2J5vg1p5b47fWetkwIWrhXyUOXE,24132
|
12
|
+
disagreement/http.py,sha256=6T1z3O6D0IL3qrUhtY-bwsnMR16a_zCMVMUU98QwKsc,31395
|
13
13
|
disagreement/hybrid_context.py,sha256=VYCmcreTZdPBU9v-Cy48W38vgWO2t8nM2ulC6_z4HjU,1095
|
14
14
|
disagreement/i18n.py,sha256=1L4rcFuKP0XjHk9dVwbNh4BkLk2ZlxxZ_-tecGWa9S0,718
|
15
15
|
disagreement/interactions.py,sha256=aUZwwEOLsEds15i6G-rxmSSDCDmaxz_cfoTYS4tv6Ao,21735
|
16
16
|
disagreement/logging_config.py,sha256=4q6baQPE6X_0lfaBTFMU1uqc03x5SbJqo2hsApdDFac,686
|
17
|
-
disagreement/models.py,sha256=
|
17
|
+
disagreement/models.py,sha256=IkPBT5Ki0IpDr2_NCIhiYSSL4aI28MakJ_PMZ81JcAA,75053
|
18
18
|
disagreement/oauth.py,sha256=TfDdCwg1J7osM9wDi61dtNBA5BrQk5DeQrrHsYycH34,2810
|
19
19
|
disagreement/permissions.py,sha256=7g5cIlg-evHXOL0-pmtT5EwqcB-stXot1HZSLz724sE,3008
|
20
|
+
disagreement/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
21
|
disagreement/rate_limiter.py,sha256=ubwR_UTPs2MotipBdtqpgwQKx0IHt2I3cdfFcXTFv7g,2521
|
21
22
|
disagreement/shard_manager.py,sha256=e9F8tx_4IEOlTX3-S3t51lfJToc6Ue3RVBzoNAiVKxs,2161
|
22
23
|
disagreement/typing.py,sha256=_1oFWfZ4HyH5Q3bnF7CO24s79z-3_B5Qb69kWiwLhhU,1242
|
@@ -30,12 +31,12 @@ disagreement/ext/app_commands/commands.py,sha256=0O5fJQg2esTQzx2FyEpM2ZrrLckNmv8
|
|
30
31
|
disagreement/ext/app_commands/context.py,sha256=Xcm4Ka5K5uTQGviixF5LeCDdOdF9YQS5F7lZi2m--8s,20831
|
31
32
|
disagreement/ext/app_commands/converters.py,sha256=J1VEmo-7H9K7kGPJodu5FX4RmFFI1BuzhlQAEs2MsD4,21036
|
32
33
|
disagreement/ext/app_commands/decorators.py,sha256=dKiD4ZEsafRoPvfgn9zuQ9vvXXo2qYTMquHvyUM1604,23251
|
33
|
-
disagreement/ext/app_commands/handler.py,sha256=
|
34
|
-
disagreement/ext/app_commands/hybrid.py,sha256=
|
34
|
+
disagreement/ext/app_commands/handler.py,sha256=36E58u8S7x-Ciop6kT6g7DdsPS2Wwo7rcvoBd7jmhWQ,26331
|
35
|
+
disagreement/ext/app_commands/hybrid.py,sha256=u3kHNbVfY3-liymgzEIkFO5YV3WM_DqwytzdN9EXWMY,3330
|
35
36
|
disagreement/ext/commands/__init__.py,sha256=HxZWVfc4qvP_bCRbKTVZoMqXFq19Gj4mQvRumvQiApQ,1130
|
36
|
-
disagreement/ext/commands/cog.py,sha256
|
37
|
+
disagreement/ext/commands/cog.py,sha256=-F2ZOXXC07r96xlt9NomRgqlIqlcxzBiha2Zhg1DVp4,6845
|
37
38
|
disagreement/ext/commands/converters.py,sha256=mh8xJr1FIiah6bdYy0KsdccfYcPii2Yc_IdhzCTw5uE,5864
|
38
|
-
disagreement/ext/commands/core.py,sha256=
|
39
|
+
disagreement/ext/commands/core.py,sha256=xHeqenDqh2xiH7n9UyZl12sI1c0erorLPwi3uusmm1E,19525
|
39
40
|
disagreement/ext/commands/decorators.py,sha256=Ox_D9KCFtMa-RiljFjOcsPb3stmDStRKeLw1DVeOdAw,6608
|
40
41
|
disagreement/ext/commands/errors.py,sha256=cG5sPA-osUq2gts5scrl5yT-BHEYVHLTb4TULjAmbaY,2065
|
41
42
|
disagreement/ext/commands/help.py,sha256=yw0ydupOsPwmnhsIIoxa93xjj9MAcBcGfD8BXa7V8G8,1456
|
@@ -46,8 +47,8 @@ disagreement/ui/item.py,sha256=bm-EmQEZpe8Kt8JrRw-o0uQdccgjErORcFsBqaXOcV8,1112
|
|
46
47
|
disagreement/ui/modal.py,sha256=w0ZEVslXzx2-RWUP4jdVB54zDuT81jpueVWZ70byFnI,4137
|
47
48
|
disagreement/ui/select.py,sha256=XjWRlWkA09QZaDDLn-wDDOWIuj0Mb4VCWJEOAaExZXw,3018
|
48
49
|
disagreement/ui/view.py,sha256=QhWoYt39QKXwl1X6Mkm5gNNEqd8bt7O505lSpiG0L04,5567
|
49
|
-
disagreement-0.1.
|
50
|
-
disagreement-0.1.
|
51
|
-
disagreement-0.1.
|
52
|
-
disagreement-0.1.
|
53
|
-
disagreement-0.1.
|
50
|
+
disagreement-0.1.0rc3.dist-info/licenses/LICENSE,sha256=zfmtgJhVFHnqT7a8LAQFthXu5bP7EEHmEL99trV66Ew,1474
|
51
|
+
disagreement-0.1.0rc3.dist-info/METADATA,sha256=4pe1v3qHLPGJD35qHIHxdI81xIiM0MdVKjS6HGsTYYs,4889
|
52
|
+
disagreement-0.1.0rc3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
53
|
+
disagreement-0.1.0rc3.dist-info/top_level.txt,sha256=t7FY_3iaYhdB6X6y9VybJ2j7UZbVeRUe9wRgH8d5Gtk,13
|
54
|
+
disagreement-0.1.0rc3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|