Replace debug prints with logging (#38)
This commit is contained in:
parent
90ee3fcf7f
commit
fd31a3162b
@ -35,7 +35,11 @@ from .enums import GatewayIntent, GatewayOpcode # Export enums
|
|||||||
from .error_handler import setup_global_error_handler
|
from .error_handler import setup_global_error_handler
|
||||||
from .hybrid_context import HybridContext
|
from .hybrid_context import HybridContext
|
||||||
from .ext import tasks
|
from .ext import tasks
|
||||||
|
from .logging_config import setup_logging
|
||||||
|
|
||||||
# Set up logging if desired
|
import logging
|
||||||
# import logging
|
|
||||||
# logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
||||||
|
# Configure a default logger if none has been configured yet
|
||||||
|
if not logging.getLogger().hasHandlers():
|
||||||
|
setup_logging(logging.INFO)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# disagreement/ext/app_commands/handler.py
|
# disagreement/ext/app_commands/handler.py
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Dict,
|
Dict,
|
||||||
@ -64,6 +65,9 @@ if not TYPE_CHECKING:
|
|||||||
Message = Any
|
Message = Any
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AppCommandHandler:
|
class AppCommandHandler:
|
||||||
"""
|
"""
|
||||||
Manages application command registration, parsing, and dispatching.
|
Manages application command registration, parsing, and dispatching.
|
||||||
@ -544,7 +548,7 @@ class AppCommandHandler:
|
|||||||
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error invoking app command '{command.name}': {e}")
|
logger.error("Error invoking app command '%s': %s", command.name, e)
|
||||||
await self.dispatch_app_command_error(ctx, e)
|
await self.dispatch_app_command_error(ctx, e)
|
||||||
# else:
|
# else:
|
||||||
# # Default error reply if no handler on client
|
# # Default error reply if no handler on client
|
||||||
@ -594,34 +598,43 @@ class AppCommandHandler:
|
|||||||
payload = cmd_or_group.to_dict()
|
payload = cmd_or_group.to_dict()
|
||||||
commands_to_sync.append(payload)
|
commands_to_sync.append(payload)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
print(
|
logger.warning(
|
||||||
f"Warning: Command or group '{cmd_or_group.name}' does not have a to_dict() method. Skipping."
|
"Command or group '%s' does not have a to_dict() method. Skipping.",
|
||||||
|
cmd_or_group.name,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
logger.error(
|
||||||
f"Error converting command/group '{cmd_or_group.name}' to dict: {e}. Skipping."
|
"Error converting command/group '%s' to dict: %s. Skipping.",
|
||||||
|
cmd_or_group.name,
|
||||||
|
e,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not commands_to_sync:
|
if not commands_to_sync:
|
||||||
print(
|
logger.info(
|
||||||
f"No commands to sync for {'guild ' + str(guild_id) if guild_id else 'global'} scope."
|
"No commands to sync for %s scope.",
|
||||||
|
f"guild {guild_id}" if guild_id else "global",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if guild_id:
|
if guild_id:
|
||||||
print(
|
logger.info(
|
||||||
f"Syncing {len(commands_to_sync)} commands for guild {guild_id}..."
|
"Syncing %s commands for guild %s...",
|
||||||
|
len(commands_to_sync),
|
||||||
|
guild_id,
|
||||||
)
|
)
|
||||||
await self.client._http.bulk_overwrite_guild_application_commands(
|
await self.client._http.bulk_overwrite_guild_application_commands(
|
||||||
application_id, guild_id, commands_to_sync
|
application_id, guild_id, commands_to_sync
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Syncing {len(commands_to_sync)} global commands...")
|
logger.info(
|
||||||
|
"Syncing %s global commands...",
|
||||||
|
len(commands_to_sync),
|
||||||
|
)
|
||||||
await self.client._http.bulk_overwrite_global_application_commands(
|
await self.client._http.bulk_overwrite_global_application_commands(
|
||||||
application_id, commands_to_sync
|
application_id, commands_to_sync
|
||||||
)
|
)
|
||||||
print("Command sync successful.")
|
logger.info("Command sync successful.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error syncing application commands: {e}")
|
logger.error("Error syncing application commands: %s", e)
|
||||||
# Consider re-raising or specific error handling
|
# Consider re-raising or specific error handling
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# disagreement/ext/commands/cog.py
|
# disagreement/ext/commands/cog.py
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, List, Tuple, Callable, Awaitable, Any, Dict, Union
|
from typing import TYPE_CHECKING, List, Tuple, Callable, Awaitable, Any, Dict, Union
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -16,6 +17,8 @@ else: # pragma: no cover - runtime imports for isinstance checks
|
|||||||
# EventDispatcher might be needed if cogs register listeners directly
|
# EventDispatcher might be needed if cogs register listeners directly
|
||||||
# from disagreement.event_dispatcher import EventDispatcher
|
# from disagreement.event_dispatcher import EventDispatcher
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Cog:
|
class Cog:
|
||||||
"""
|
"""
|
||||||
@ -59,8 +62,10 @@ class Cog:
|
|||||||
cmd.cog = self # Assign the cog instance to the command
|
cmd.cog = self # Assign the cog instance to the command
|
||||||
if cmd.name in self._commands:
|
if cmd.name in self._commands:
|
||||||
# This should ideally be caught earlier or handled by CommandHandler
|
# This should ideally be caught earlier or handled by CommandHandler
|
||||||
print(
|
logger.warning(
|
||||||
f"Warning: Duplicate command name '{cmd.name}' in cog '{self.cog_name}'. Overwriting."
|
"Duplicate command name '%s' in cog '%s'. Overwriting.",
|
||||||
|
cmd.name,
|
||||||
|
self.cog_name,
|
||||||
)
|
)
|
||||||
self._commands[cmd.name.lower()] = cmd
|
self._commands[cmd.name.lower()] = cmd
|
||||||
# Also register aliases
|
# Also register aliases
|
||||||
@ -79,8 +84,10 @@ class Cog:
|
|||||||
# For AppCommandGroup, its commands will have cog set individually if they are AppCommands
|
# For AppCommandGroup, its commands will have cog set individually if they are AppCommands
|
||||||
self._app_commands_and_groups.append(app_cmd_obj)
|
self._app_commands_and_groups.append(app_cmd_obj)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.warning(
|
||||||
f"Warning: Member '{member_name}' in cog '{self.cog_name}' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup."
|
"Member '%s' in cog '%s' has '__app_command_object__' but it's not an AppCommand or AppCommandGroup.",
|
||||||
|
member_name,
|
||||||
|
self.cog_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(member, (AppCommand, AppCommandGroup)):
|
elif isinstance(member, (AppCommand, AppCommandGroup)):
|
||||||
@ -92,8 +99,10 @@ class Cog:
|
|||||||
# This is a method decorated with @commands.Cog.listener or @commands.listener
|
# This is a method decorated with @commands.Cog.listener or @commands.listener
|
||||||
if not inspect.iscoroutinefunction(member):
|
if not inspect.iscoroutinefunction(member):
|
||||||
# Decorator should have caught this, but double check
|
# Decorator should have caught this, but double check
|
||||||
print(
|
logger.warning(
|
||||||
f"Warning: Listener '{member_name}' in cog '{self.cog_name}' is not a coroutine. Skipping."
|
"Listener '%s' in cog '%s' is not a coroutine. Skipping.",
|
||||||
|
member_name,
|
||||||
|
self.cog_name,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import inspect
|
import inspect
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
@ -31,6 +32,8 @@ from .errors import (
|
|||||||
from .converters import run_converters, DEFAULT_CONVERTERS, Converter
|
from .converters import run_converters, DEFAULT_CONVERTERS, Converter
|
||||||
from disagreement.typing import Typing
|
from disagreement.typing import Typing
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .cog import Cog
|
from .cog import Cog
|
||||||
from disagreement.client import Client
|
from disagreement.client import Client
|
||||||
@ -224,8 +227,10 @@ class CommandHandler:
|
|||||||
self.commands[command.name.lower()] = command
|
self.commands[command.name.lower()] = command
|
||||||
for alias in command.aliases:
|
for alias in command.aliases:
|
||||||
if alias in self.commands:
|
if alias in self.commands:
|
||||||
print(
|
logger.warning(
|
||||||
f"Warning: Alias '{alias}' for command '{command.name}' conflicts with an existing command or alias."
|
"Alias '%s' for command '%s' conflicts with an existing command or alias.",
|
||||||
|
alias,
|
||||||
|
command.name,
|
||||||
)
|
)
|
||||||
self.commands[alias.lower()] = command
|
self.commands[alias.lower()] = command
|
||||||
|
|
||||||
@ -241,6 +246,7 @@ class CommandHandler:
|
|||||||
|
|
||||||
def add_cog(self, cog_to_add: "Cog") -> None:
|
def add_cog(self, cog_to_add: "Cog") -> None:
|
||||||
from .cog import Cog
|
from .cog import Cog
|
||||||
|
|
||||||
if not isinstance(cog_to_add, Cog):
|
if not isinstance(cog_to_add, Cog):
|
||||||
raise TypeError("Argument must be a subclass of Cog.")
|
raise TypeError("Argument must be a subclass of Cog.")
|
||||||
|
|
||||||
@ -258,8 +264,9 @@ class CommandHandler:
|
|||||||
for event_name, callback in cog_to_add.get_listeners():
|
for event_name, callback in cog_to_add.get_listeners():
|
||||||
self.client._event_dispatcher.register(event_name.upper(), callback)
|
self.client._event_dispatcher.register(event_name.upper(), callback)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.warning(
|
||||||
f"Warning: Client does not have '_event_dispatcher'. Listeners for cog '{cog_to_add.cog_name}' not registered."
|
"Client does not have '_event_dispatcher'. Listeners for cog '%s' not registered.",
|
||||||
|
cog_to_add.cog_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(cog_to_add, "cog_load") and inspect.iscoroutinefunction(
|
if hasattr(cog_to_add, "cog_load") and inspect.iscoroutinefunction(
|
||||||
@ -267,7 +274,7 @@ class CommandHandler:
|
|||||||
):
|
):
|
||||||
asyncio.create_task(cog_to_add.cog_load())
|
asyncio.create_task(cog_to_add.cog_load())
|
||||||
|
|
||||||
print(f"Cog '{cog_to_add.cog_name}' added.")
|
logger.info("Cog '%s' added.", cog_to_add.cog_name)
|
||||||
|
|
||||||
def remove_cog(self, cog_name: str) -> Optional["Cog"]:
|
def remove_cog(self, cog_name: str) -> Optional["Cog"]:
|
||||||
cog_to_remove = self.cogs.pop(cog_name, None)
|
cog_to_remove = self.cogs.pop(cog_name, None)
|
||||||
@ -277,8 +284,11 @@ class CommandHandler:
|
|||||||
|
|
||||||
if hasattr(self.client, "_event_dispatcher"):
|
if hasattr(self.client, "_event_dispatcher"):
|
||||||
for event_name, callback in cog_to_remove.get_listeners():
|
for event_name, callback in cog_to_remove.get_listeners():
|
||||||
print(
|
logger.debug(
|
||||||
f"Note: Listener '{callback.__name__}' for event '{event_name}' from cog '{cog_name}' needs manual unregistration logic in EventDispatcher."
|
"Listener '%s' for event '%s' from cog '%s' needs manual unregistration logic in EventDispatcher.",
|
||||||
|
callback.__name__,
|
||||||
|
event_name,
|
||||||
|
cog_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(cog_to_remove, "cog_unload") and inspect.iscoroutinefunction(
|
if hasattr(cog_to_remove, "cog_unload") and inspect.iscoroutinefunction(
|
||||||
@ -287,7 +297,7 @@ class CommandHandler:
|
|||||||
asyncio.create_task(cog_to_remove.cog_unload())
|
asyncio.create_task(cog_to_remove.cog_unload())
|
||||||
|
|
||||||
cog_to_remove._eject()
|
cog_to_remove._eject()
|
||||||
print(f"Cog '{cog_name}' removed.")
|
logger.info("Cog '%s' removed.", cog_name)
|
||||||
return cog_to_remove
|
return cog_to_remove
|
||||||
|
|
||||||
async def get_prefix(self, message: "Message") -> Union[str, List[str], None]:
|
async def get_prefix(self, message: "Message") -> Union[str, List[str], None]:
|
||||||
@ -493,11 +503,11 @@ class CommandHandler:
|
|||||||
ctx.kwargs = parsed_kwargs
|
ctx.kwargs = parsed_kwargs
|
||||||
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
await command.invoke(ctx, *parsed_args, **parsed_kwargs)
|
||||||
except CommandError as e:
|
except CommandError as e:
|
||||||
print(f"Command error for '{command.name}': {e}")
|
logger.error("Command error for '%s': %s", command.name, e)
|
||||||
if hasattr(self.client, "on_command_error"):
|
if hasattr(self.client, "on_command_error"):
|
||||||
await self.client.on_command_error(ctx, e)
|
await self.client.on_command_error(ctx, e)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Unexpected error invoking command '{command.name}': {e}")
|
logger.error("Unexpected error invoking command '%s': %s", command.name, e)
|
||||||
exc = CommandInvokeError(e)
|
exc = CommandInvokeError(e)
|
||||||
if hasattr(self.client, "on_command_error"):
|
if hasattr(self.client, "on_command_error"):
|
||||||
await self.client.on_command_error(ctx, exc)
|
await self.client.on_command_error(ctx, exc)
|
||||||
|
@ -5,6 +5,7 @@ Manages the WebSocket connection to the Discord Gateway.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import json
|
import json
|
||||||
@ -28,6 +29,9 @@ ZLIB_SUFFIX = b"\x00\x00\xff\xff"
|
|||||||
MAX_DECOMPRESSION_SIZE = 10 * 1024 * 1024 # 10 MiB, adjust as needed
|
MAX_DECOMPRESSION_SIZE = 10 * 1024 * 1024 # 10 MiB, adjust as needed
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GatewayClient:
|
class GatewayClient:
|
||||||
"""
|
"""
|
||||||
Handles the Discord Gateway WebSocket connection, heartbeating, and event dispatching.
|
Handles the Discord Gateway WebSocket connection, heartbeating, and event dispatching.
|
||||||
@ -84,13 +88,17 @@ class GatewayClient:
|
|||||||
return
|
return
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
if attempt >= self._max_retries - 1:
|
if attempt >= self._max_retries - 1:
|
||||||
print(f"Reconnect failed after {attempt + 1} attempts: {e}")
|
logger.error(
|
||||||
|
"Reconnect failed after %s attempts: %s", attempt + 1, e
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
jitter = random.uniform(0, delay)
|
jitter = random.uniform(0, delay)
|
||||||
wait_time = min(delay + jitter, self._max_backoff)
|
wait_time = min(delay + jitter, self._max_backoff)
|
||||||
print(
|
logger.warning(
|
||||||
f"Reconnect attempt {attempt + 1} failed: {e}. "
|
"Reconnect attempt %s failed: %s. Retrying in %.2f seconds...",
|
||||||
f"Retrying in {wait_time:.2f} seconds..."
|
attempt + 1,
|
||||||
|
e,
|
||||||
|
wait_time,
|
||||||
)
|
)
|
||||||
await asyncio.sleep(wait_time)
|
await asyncio.sleep(wait_time)
|
||||||
delay = min(delay * 2, self._max_backoff)
|
delay = min(delay * 2, self._max_backoff)
|
||||||
@ -112,21 +120,23 @@ class GatewayClient:
|
|||||||
self._buffer.clear() # Reset buffer after successful decompression
|
self._buffer.clear() # Reset buffer after successful decompression
|
||||||
return json.loads(decompressed.decode("utf-8"))
|
return json.loads(decompressed.decode("utf-8"))
|
||||||
except zlib.error as e:
|
except zlib.error as e:
|
||||||
print(f"Zlib decompression error: {e}")
|
logger.error("Zlib decompression error: %s", e)
|
||||||
self._buffer.clear() # Clear buffer on error
|
self._buffer.clear() # Clear buffer on error
|
||||||
self._inflator = zlib.decompressobj() # Reset inflator
|
self._inflator = zlib.decompressobj() # Reset inflator
|
||||||
return None
|
return None
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print(f"JSON decode error after decompression: {e}")
|
logger.error("JSON decode error after decompression: %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _send_json(self, payload: Dict[str, Any]):
|
async def _send_json(self, payload: Dict[str, Any]):
|
||||||
if self._ws and not self._ws.closed:
|
if self._ws and not self._ws.closed:
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print(f"GATEWAY SEND: {payload}")
|
logger.debug("GATEWAY SEND: %s", payload)
|
||||||
await self._ws.send_json(payload)
|
await self._ws.send_json(payload)
|
||||||
else:
|
else:
|
||||||
print("Gateway send attempted but WebSocket is closed or not available.")
|
logger.warning(
|
||||||
|
"Gateway send attempted but WebSocket is closed or not available."
|
||||||
|
)
|
||||||
# raise GatewayException("WebSocket is not connected.")
|
# raise GatewayException("WebSocket is not connected.")
|
||||||
|
|
||||||
async def _heartbeat(self):
|
async def _heartbeat(self):
|
||||||
@ -140,7 +150,7 @@ class GatewayClient:
|
|||||||
"""Manages the heartbeating loop."""
|
"""Manages the heartbeating loop."""
|
||||||
if self._heartbeat_interval is None:
|
if self._heartbeat_interval is None:
|
||||||
# This should not happen if HELLO was processed correctly
|
# This should not happen if HELLO was processed correctly
|
||||||
print("Error: Heartbeat interval not set. Cannot start keep_alive.")
|
logger.error("Heartbeat interval not set. Cannot start keep_alive.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -150,9 +160,9 @@ class GatewayClient:
|
|||||||
self._heartbeat_interval / 1000
|
self._heartbeat_interval / 1000
|
||||||
) # Interval is in ms
|
) # Interval is in ms
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
print("Keep_alive task cancelled.")
|
logger.debug("Keep_alive task cancelled.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in keep_alive loop: {e}")
|
logger.error("Error in keep_alive loop: %s", e)
|
||||||
# Potentially trigger a reconnect here or notify client
|
# Potentially trigger a reconnect here or notify client
|
||||||
await self._client_instance.close_gateway(code=1000) # Generic close
|
await self._client_instance.close_gateway(code=1000) # Generic close
|
||||||
|
|
||||||
@ -174,12 +184,12 @@ class GatewayClient:
|
|||||||
if self._shard_id is not None and self._shard_count is not None:
|
if self._shard_id is not None and self._shard_count is not None:
|
||||||
payload["d"]["shard"] = [self._shard_id, self._shard_count]
|
payload["d"]["shard"] = [self._shard_id, self._shard_count]
|
||||||
await self._send_json(payload)
|
await self._send_json(payload)
|
||||||
print("Sent IDENTIFY.")
|
logger.info("Sent IDENTIFY.")
|
||||||
|
|
||||||
async def _resume(self):
|
async def _resume(self):
|
||||||
"""Sends the RESUME payload to the Gateway."""
|
"""Sends the RESUME payload to the Gateway."""
|
||||||
if not self._session_id or self._last_sequence is None:
|
if not self._session_id or self._last_sequence is None:
|
||||||
print("Cannot RESUME: session_id or last_sequence is missing.")
|
logger.warning("Cannot RESUME: session_id or last_sequence is missing.")
|
||||||
await self._identify() # Fallback to identify
|
await self._identify() # Fallback to identify
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -192,8 +202,10 @@ class GatewayClient:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
await self._send_json(payload)
|
await self._send_json(payload)
|
||||||
print(
|
logger.info(
|
||||||
f"Sent RESUME for session {self._session_id} at sequence {self._last_sequence}."
|
"Sent RESUME for session %s at sequence %s.",
|
||||||
|
self._session_id,
|
||||||
|
self._last_sequence,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def update_presence(
|
async def update_presence(
|
||||||
@ -238,8 +250,9 @@ class GatewayClient:
|
|||||||
|
|
||||||
if event_name == "READY": # Special handling for READY
|
if event_name == "READY": # Special handling for READY
|
||||||
if not isinstance(raw_event_d_payload, dict):
|
if not isinstance(raw_event_d_payload, dict):
|
||||||
print(
|
logger.error(
|
||||||
f"Error: READY event 'd' payload is not a dict or is missing: {raw_event_d_payload}"
|
"READY event 'd' payload is not a dict or is missing: %s",
|
||||||
|
raw_event_d_payload,
|
||||||
)
|
)
|
||||||
# Consider raising an error or attempting a reconnect
|
# Consider raising an error or attempting a reconnect
|
||||||
return
|
return
|
||||||
@ -259,8 +272,8 @@ class GatewayClient:
|
|||||||
)
|
)
|
||||||
app_id_str = str(app_id_value)
|
app_id_str = str(app_id_value)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.warning(
|
||||||
f"Warning: Could not find application ID in READY payload. App commands may not work."
|
"Could not find application ID in READY payload. App commands may not work."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Parse and store the bot's own user object
|
# Parse and store the bot's own user object
|
||||||
@ -274,20 +287,29 @@ class GatewayClient:
|
|||||||
raw_event_d_payload["user"]
|
raw_event_d_payload["user"]
|
||||||
)
|
)
|
||||||
self._client_instance.user = bot_user_obj
|
self._client_instance.user = bot_user_obj
|
||||||
print(
|
logger.info(
|
||||||
f"Gateway READY. Bot User: {bot_user_obj.username}#{bot_user_obj.discriminator}. Session ID: {self._session_id}. App ID: {app_id_str}. Resume URL: {self._resume_gateway_url}"
|
"Gateway READY. Bot User: %s#%s. Session ID: %s. App ID: %s. Resume URL: %s",
|
||||||
|
bot_user_obj.username,
|
||||||
|
bot_user_obj.discriminator,
|
||||||
|
self._session_id,
|
||||||
|
app_id_str,
|
||||||
|
self._resume_gateway_url,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error parsing bot user from READY payload: {e}")
|
logger.error("Error parsing bot user from READY payload: %s", e)
|
||||||
print(
|
logger.info(
|
||||||
f"Gateway READY (user parse failed). Session ID: {self._session_id}. App ID: {app_id_str}. Resume URL: {self._resume_gateway_url}"
|
"Gateway READY (user parse failed). Session ID: %s. App ID: %s. Resume URL: %s",
|
||||||
|
self._session_id,
|
||||||
|
app_id_str,
|
||||||
|
self._resume_gateway_url,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.warning("Bot user object not found or invalid in READY payload.")
|
||||||
f"Warning: Bot user object not found or invalid in READY payload."
|
logger.info(
|
||||||
)
|
"Gateway READY (no user). Session ID: %s. App ID: %s. Resume URL: %s",
|
||||||
print(
|
self._session_id,
|
||||||
f"Gateway READY (no user). Session ID: {self._session_id}. App ID: {app_id_str}. Resume URL: {self._resume_gateway_url}"
|
app_id_str,
|
||||||
|
self._resume_gateway_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._dispatcher.dispatch(event_name, raw_event_d_payload)
|
await self._dispatcher.dispatch(event_name, raw_event_d_payload)
|
||||||
@ -306,15 +328,16 @@ class GatewayClient:
|
|||||||
self._client_instance.process_interaction(interaction)
|
self._client_instance.process_interaction(interaction)
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
else:
|
else:
|
||||||
print(
|
logger.warning(
|
||||||
"Warning: Client instance does not have process_interaction method for INTERACTION_CREATE."
|
"Client instance does not have process_interaction method for INTERACTION_CREATE."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.error(
|
||||||
f"Error: INTERACTION_CREATE event 'd' payload is not a dict: {raw_event_d_payload}"
|
"INTERACTION_CREATE event 'd' payload is not a dict: %s",
|
||||||
|
raw_event_d_payload,
|
||||||
)
|
)
|
||||||
elif event_name == "RESUMED":
|
elif event_name == "RESUMED":
|
||||||
print("Gateway RESUMED successfully.")
|
logger.info("Gateway RESUMED successfully.")
|
||||||
# RESUMED 'd' payload is often an empty object or debug info.
|
# RESUMED 'd' payload is often an empty object or debug info.
|
||||||
# Ensure it's a dict for the dispatcher.
|
# Ensure it's a dict for the dispatcher.
|
||||||
event_data_to_dispatch = (
|
event_data_to_dispatch = (
|
||||||
@ -330,7 +353,7 @@ class GatewayClient:
|
|||||||
# print(f"GATEWAY RECV EVENT: {event_name} | DATA: {event_data_to_dispatch}")
|
# print(f"GATEWAY RECV EVENT: {event_name} | DATA: {event_data_to_dispatch}")
|
||||||
await self._dispatcher.dispatch(event_name, event_data_to_dispatch)
|
await self._dispatcher.dispatch(event_name, event_data_to_dispatch)
|
||||||
else:
|
else:
|
||||||
print(f"Received dispatch with no event name: {data}")
|
logger.warning("Received dispatch with no event name: %s", data)
|
||||||
|
|
||||||
async def _process_message(self, msg: aiohttp.WSMessage):
|
async def _process_message(self, msg: aiohttp.WSMessage):
|
||||||
"""Processes a single message from the WebSocket."""
|
"""Processes a single message from the WebSocket."""
|
||||||
@ -338,19 +361,20 @@ class GatewayClient:
|
|||||||
try:
|
try:
|
||||||
data = json.loads(msg.data)
|
data = json.loads(msg.data)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(
|
logger.error("Failed to decode JSON from Gateway: %s", msg.data[:200])
|
||||||
f"Failed to decode JSON from Gateway: {msg.data[:200]}"
|
|
||||||
) # Log snippet
|
|
||||||
return
|
return
|
||||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||||
decompressed_data = await self._decompress_message(msg.data)
|
decompressed_data = await self._decompress_message(msg.data)
|
||||||
if decompressed_data is None:
|
if decompressed_data is None:
|
||||||
print("Failed to decompress or decode binary message from Gateway.")
|
logger.error(
|
||||||
|
"Failed to decompress or decode binary message from Gateway."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
data = decompressed_data
|
data = decompressed_data
|
||||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||||
print(
|
logger.error(
|
||||||
f"WebSocket error: {self._ws.exception() if self._ws else 'Unknown WSError'}"
|
"WebSocket error: %s",
|
||||||
|
self._ws.exception() if self._ws else "Unknown WSError",
|
||||||
)
|
)
|
||||||
raise GatewayException(
|
raise GatewayException(
|
||||||
f"WebSocket error: {self._ws.exception() if self._ws else 'Unknown WSError'}"
|
f"WebSocket error: {self._ws.exception() if self._ws else 'Unknown WSError'}"
|
||||||
@ -361,15 +385,17 @@ class GatewayClient:
|
|||||||
if self._ws and hasattr(self._ws, "close_code")
|
if self._ws and hasattr(self._ws, "close_code")
|
||||||
else "N/A"
|
else "N/A"
|
||||||
)
|
)
|
||||||
print(f"WebSocket connection closed by server. Code: {close_code}")
|
logger.warning(
|
||||||
|
"WebSocket connection closed by server. Code: %s", close_code
|
||||||
|
)
|
||||||
# Raise an exception to signal the closure to the client's main run loop
|
# Raise an exception to signal the closure to the client's main run loop
|
||||||
raise GatewayException(f"WebSocket closed by server. Code: {close_code}")
|
raise GatewayException(f"WebSocket closed by server. Code: {close_code}")
|
||||||
else:
|
else:
|
||||||
print(f"Received unhandled WebSocket message type: {msg.type}")
|
logger.warning("Received unhandled WebSocket message type: %s", msg.type)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print(f"GATEWAY RECV: {data}")
|
logger.debug("GATEWAY RECV: %s", data)
|
||||||
op = data.get("op")
|
op = data.get("op")
|
||||||
# 'd' payload (event_data) is handled specifically by each opcode handler below
|
# 'd' payload (event_data) is handled specifically by each opcode handler below
|
||||||
|
|
||||||
@ -378,12 +404,16 @@ class GatewayClient:
|
|||||||
elif op == GatewayOpcode.HEARTBEAT: # Server requests a heartbeat
|
elif op == GatewayOpcode.HEARTBEAT: # Server requests a heartbeat
|
||||||
await self._heartbeat()
|
await self._heartbeat()
|
||||||
elif op == GatewayOpcode.RECONNECT: # Server requests a reconnect
|
elif op == GatewayOpcode.RECONNECT: # Server requests a reconnect
|
||||||
print("Gateway requested RECONNECT. Closing and will attempt to reconnect.")
|
logger.info(
|
||||||
|
"Gateway requested RECONNECT. Closing and will attempt to reconnect."
|
||||||
|
)
|
||||||
await self.close(code=4000, reconnect=True)
|
await self.close(code=4000, reconnect=True)
|
||||||
elif op == GatewayOpcode.INVALID_SESSION:
|
elif op == GatewayOpcode.INVALID_SESSION:
|
||||||
# The 'd' payload for INVALID_SESSION is a boolean indicating resumability
|
# The 'd' payload for INVALID_SESSION is a boolean indicating resumability
|
||||||
can_resume = data.get("d") is True
|
can_resume = data.get("d") is True
|
||||||
print(f"Gateway indicated INVALID_SESSION. Resumable: {can_resume}")
|
logger.warning(
|
||||||
|
"Gateway indicated INVALID_SESSION. Resumable: %s", can_resume
|
||||||
|
)
|
||||||
if not can_resume:
|
if not can_resume:
|
||||||
self._session_id = None # Clear session_id to force re-identify
|
self._session_id = None # Clear session_id to force re-identify
|
||||||
self._last_sequence = None
|
self._last_sequence = None
|
||||||
@ -395,13 +425,16 @@ class GatewayClient:
|
|||||||
not isinstance(hello_d_payload, dict)
|
not isinstance(hello_d_payload, dict)
|
||||||
or "heartbeat_interval" not in hello_d_payload
|
or "heartbeat_interval" not in hello_d_payload
|
||||||
):
|
):
|
||||||
print(
|
logger.error(
|
||||||
f"Error: HELLO event 'd' payload is invalid or missing heartbeat_interval: {hello_d_payload}"
|
"HELLO event 'd' payload is invalid or missing heartbeat_interval: %s",
|
||||||
|
hello_d_payload,
|
||||||
)
|
)
|
||||||
await self.close(code=1011) # Internal error, malformed HELLO
|
await self.close(code=1011) # Internal error, malformed HELLO
|
||||||
return
|
return
|
||||||
self._heartbeat_interval = hello_d_payload["heartbeat_interval"]
|
self._heartbeat_interval = hello_d_payload["heartbeat_interval"]
|
||||||
print(f"Gateway HELLO. Heartbeat interval: {self._heartbeat_interval}ms.")
|
logger.info(
|
||||||
|
"Gateway HELLO. Heartbeat interval: %sms.", self._heartbeat_interval
|
||||||
|
)
|
||||||
# Start heartbeating
|
# Start heartbeating
|
||||||
if self._keep_alive_task:
|
if self._keep_alive_task:
|
||||||
self._keep_alive_task.cancel()
|
self._keep_alive_task.cancel()
|
||||||
@ -409,45 +442,51 @@ class GatewayClient:
|
|||||||
|
|
||||||
# Identify or Resume
|
# Identify or Resume
|
||||||
if self._session_id and self._resume_gateway_url: # Check if we can resume
|
if self._session_id and self._resume_gateway_url: # Check if we can resume
|
||||||
print("Attempting to RESUME session.")
|
logger.info("Attempting to RESUME session.")
|
||||||
await self._resume()
|
await self._resume()
|
||||||
else:
|
else:
|
||||||
print("Performing initial IDENTIFY.")
|
logger.info("Performing initial IDENTIFY.")
|
||||||
await self._identify()
|
await self._identify()
|
||||||
elif op == GatewayOpcode.HEARTBEAT_ACK:
|
elif op == GatewayOpcode.HEARTBEAT_ACK:
|
||||||
self._last_heartbeat_ack = time.monotonic()
|
self._last_heartbeat_ack = time.monotonic()
|
||||||
# print("Received heartbeat ACK.")
|
# print("Received heartbeat ACK.")
|
||||||
pass # Good, connection is alive
|
pass # Good, connection is alive
|
||||||
else:
|
else:
|
||||||
print(f"Received unhandled Gateway Opcode: {op} with data: {data}")
|
logger.warning(
|
||||||
|
"Received unhandled Gateway Opcode: %s with data: %s", op, data
|
||||||
|
)
|
||||||
|
|
||||||
async def _receive_loop(self):
|
async def _receive_loop(self):
|
||||||
"""Continuously receives and processes messages from the WebSocket."""
|
"""Continuously receives and processes messages from the WebSocket."""
|
||||||
if not self._ws or self._ws.closed:
|
if not self._ws or self._ws.closed:
|
||||||
print("Receive loop cannot start: WebSocket is not connected or closed.")
|
logger.warning(
|
||||||
|
"Receive loop cannot start: WebSocket is not connected or closed."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for msg in self._ws:
|
async for msg in self._ws:
|
||||||
await self._process_message(msg)
|
await self._process_message(msg)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
print("Receive_loop task cancelled.")
|
logger.debug("Receive_loop task cancelled.")
|
||||||
except aiohttp.ClientConnectionError as e:
|
except aiohttp.ClientConnectionError as e:
|
||||||
print(f"ClientConnectionError in receive_loop: {e}. Attempting reconnect.")
|
logger.warning(
|
||||||
|
"ClientConnectionError in receive_loop: %s. Attempting reconnect.", e
|
||||||
|
)
|
||||||
await self.close(code=1006, reconnect=True) # Abnormal closure
|
await self.close(code=1006, reconnect=True) # Abnormal closure
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Unexpected error in receive_loop: {e}")
|
logger.error("Unexpected error in receive_loop: %s", e)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
await self.close(code=1011, reconnect=True)
|
await self.close(code=1011, reconnect=True)
|
||||||
finally:
|
finally:
|
||||||
print("Receive_loop ended.")
|
logger.info("Receive_loop ended.")
|
||||||
# If the loop ends unexpectedly (not due to explicit close),
|
# If the loop ends unexpectedly (not due to explicit close),
|
||||||
# the main client might want to try reconnecting.
|
# the main client might want to try reconnecting.
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connects to the Discord Gateway."""
|
"""Connects to the Discord Gateway."""
|
||||||
if self._ws and not self._ws.closed:
|
if self._ws and not self._ws.closed:
|
||||||
print("Gateway already connected or connecting.")
|
logger.warning("Gateway already connected or connecting.")
|
||||||
return
|
return
|
||||||
|
|
||||||
gateway_url = (
|
gateway_url = (
|
||||||
@ -456,14 +495,14 @@ class GatewayClient:
|
|||||||
if not gateway_url.endswith("?v=10&encoding=json&compress=zlib-stream"):
|
if not gateway_url.endswith("?v=10&encoding=json&compress=zlib-stream"):
|
||||||
gateway_url += "?v=10&encoding=json&compress=zlib-stream"
|
gateway_url += "?v=10&encoding=json&compress=zlib-stream"
|
||||||
|
|
||||||
print(f"Connecting to Gateway: {gateway_url}")
|
logger.info("Connecting to Gateway: %s", gateway_url)
|
||||||
try:
|
try:
|
||||||
await self._http._ensure_session() # Ensure the HTTP client's session is active
|
await self._http._ensure_session() # Ensure the HTTP client's session is active
|
||||||
assert (
|
assert (
|
||||||
self._http._session is not None
|
self._http._session is not None
|
||||||
), "HTTPClient session not initialized after ensure_session"
|
), "HTTPClient session not initialized after ensure_session"
|
||||||
self._ws = await self._http._session.ws_connect(gateway_url, max_msg_size=0)
|
self._ws = await self._http._session.ws_connect(gateway_url, max_msg_size=0)
|
||||||
print("Gateway WebSocket connection established.")
|
logger.info("Gateway WebSocket connection established.")
|
||||||
|
|
||||||
if self._receive_task:
|
if self._receive_task:
|
||||||
self._receive_task.cancel()
|
self._receive_task.cancel()
|
||||||
@ -488,7 +527,7 @@ class GatewayClient:
|
|||||||
|
|
||||||
async def close(self, code: int = 1000, *, reconnect: bool = False):
|
async def close(self, code: int = 1000, *, reconnect: bool = False):
|
||||||
"""Closes the Gateway connection."""
|
"""Closes the Gateway connection."""
|
||||||
print(f"Closing Gateway connection with code {code}...")
|
logger.info("Closing Gateway connection with code %s...", code)
|
||||||
if self._keep_alive_task and not self._keep_alive_task.done():
|
if self._keep_alive_task and not self._keep_alive_task.done():
|
||||||
self._keep_alive_task.cancel()
|
self._keep_alive_task.cancel()
|
||||||
try:
|
try:
|
||||||
@ -507,7 +546,7 @@ class GatewayClient:
|
|||||||
|
|
||||||
if self._ws and not self._ws.closed:
|
if self._ws and not self._ws.closed:
|
||||||
await self._ws.close(code=code)
|
await self._ws.close(code=code)
|
||||||
print("Gateway WebSocket closed.")
|
logger.info("Gateway WebSocket closed.")
|
||||||
|
|
||||||
self._ws = None
|
self._ws = None
|
||||||
# Do not reset session_id, last_sequence, or resume_gateway_url here
|
# Do not reset session_id, last_sequence, or resume_gateway_url here
|
||||||
@ -515,7 +554,7 @@ class GatewayClient:
|
|||||||
# The connect logic will decide whether to resume or re-identify.
|
# The connect logic will decide whether to resume or re-identify.
|
||||||
# However, if it's a non-resumable close (e.g. Invalid Session non-resumable), clear them.
|
# However, if it's a non-resumable close (e.g. Invalid Session non-resumable), clear them.
|
||||||
if code == 4009: # Invalid session, not resumable
|
if code == 4009: # Invalid session, not resumable
|
||||||
print("Clearing session state due to non-resumable invalid session.")
|
logger.info("Clearing session state due to non-resumable invalid session.")
|
||||||
self._session_id = None
|
self._session_id = None
|
||||||
self._last_sequence = None
|
self._last_sequence = None
|
||||||
self._resume_gateway_url = None # This might be re-fetched anyway
|
self._resume_gateway_url = None # This might be re-fetched anyway
|
||||||
|
@ -5,6 +5,7 @@ HTTP client for interacting with the Discord REST API.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import aiohttp # pylint: disable=import-error
|
import aiohttp # pylint: disable=import-error
|
||||||
import json
|
import json
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@ -28,6 +29,8 @@ if TYPE_CHECKING:
|
|||||||
# Discord API constants
|
# Discord API constants
|
||||||
API_BASE_URL = "https://discord.com/api/v10" # Using API v10
|
API_BASE_URL = "https://discord.com/api/v10" # Using API v10
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient:
|
class HTTPClient:
|
||||||
"""Handles HTTP requests to the Discord API."""
|
"""Handles HTTP requests to the Discord API."""
|
||||||
@ -86,7 +89,13 @@ class HTTPClient:
|
|||||||
final_headers.update(custom_headers)
|
final_headers.update(custom_headers)
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print(f"HTTP REQUEST: {method} {url} | payload={payload} params={params}")
|
logger.debug(
|
||||||
|
"HTTP REQUEST: %s %s | payload=%s params=%s",
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
payload,
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
|
||||||
route = f"{method.upper()}:{endpoint}"
|
route = f"{method.upper()}:{endpoint}"
|
||||||
|
|
||||||
@ -119,7 +128,9 @@ class HTTPClient:
|
|||||||
) # Fallback to text if JSON parsing fails
|
) # Fallback to text if JSON parsing fails
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print(f"HTTP RESPONSE: {response.status} {url} | {data}")
|
logger.debug(
|
||||||
|
"HTTP RESPONSE: %s %s | %s", response.status, url, data
|
||||||
|
)
|
||||||
|
|
||||||
self._rate_limiter.release(route, response.headers)
|
self._rate_limiter.release(route, response.headers)
|
||||||
|
|
||||||
@ -150,8 +161,12 @@ class HTTPClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if attempt < 4: # Don't log on the last attempt before raising
|
if attempt < 4: # Don't log on the last attempt before raising
|
||||||
print(
|
logger.warning(
|
||||||
f"{error_message} Retrying after {retry_after}s (Attempt {attempt + 1}/5). Global: {is_global}"
|
"%s Retrying after %ss (Attempt %s/5). Global: %s",
|
||||||
|
error_message,
|
||||||
|
retry_after,
|
||||||
|
attempt + 1,
|
||||||
|
is_global,
|
||||||
)
|
)
|
||||||
continue # Retry the request
|
continue # Retry the request
|
||||||
else: # Last attempt failed
|
else: # Last attempt failed
|
||||||
|
Loading…
x
Reference in New Issue
Block a user