feat: emit raw gateway events (#15)

This commit is contained in:
Slipstream 2025-06-10 15:45:14 -06:00 committed by GitHub
parent 4c203f6792
commit 06f972851b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 81 additions and 60 deletions

View File

@ -236,21 +236,54 @@ class EventDispatcher:
if not waiters:
self._waiters.pop(event_name, None)
async def dispatch(self, event_name: str, raw_data: Dict[str, Any]):
"""
Dispatches an event to all registered listeners.
Args:
event_name (str): The name of the event (e.g., 'MESSAGE_CREATE').
raw_data (Dict[str, Any]): The raw data payload from the Discord Gateway for this event.
"""
event_name_upper = event_name.upper()
listeners = self._listeners.get(event_name_upper)
async def _dispatch_to_listeners(self, event_name: str, data: Any) -> None:
listeners = self._listeners.get(event_name)
if not listeners:
# print(f"No listeners for event {event_name_upper}")
return
self._resolve_waiters(event_name, data)
for listener in listeners:
try:
sig = inspect.signature(listener)
num_params = len(sig.parameters)
if num_params == 0:
await listener()
elif num_params == 1:
await listener(data)
else:
print(
f"Warning: Listener {listener.__name__} for {event_name} has an unhandled number of parameters ({num_params}). Skipping or attempting with one arg."
)
if num_params > 0:
await listener(data)
except Exception as e:
callback = self.on_dispatch_error
if callback is not None:
try:
await callback(event_name, e, listener)
except Exception as hook_error:
print(f"Error in on_dispatch_error hook itself: {hook_error}")
else:
print(
f"Error in event listener {listener.__name__} for {event_name}: {e}"
)
if hasattr(self._client, "on_error"):
try:
await self._client.on_error(event_name, e, listener)
except Exception as client_err_e:
print(f"Error in client.on_error itself: {client_err_e}")
async def dispatch(self, event_name: str, raw_data: Dict[str, Any]):
"""Dispatch an event and its raw counterpart to all listeners."""
event_name_upper = event_name.upper()
raw_event_name = f"RAW_{event_name_upper}"
await self._dispatch_to_listeners(raw_event_name, raw_data)
parsed_data: Any = raw_data
if event_name_upper in self._event_parsers:
try:
@ -258,53 +291,6 @@ class EventDispatcher:
parsed_data = parser(raw_data)
except Exception as e:
print(f"Error parsing event data for {event_name_upper}: {e}")
# Optionally, dispatch with raw_data or raise, or log more formally
# For now, we'll proceed to dispatch with raw_data if parsing fails,
# or just log and return if parsed_data is critical.
# Let's assume if a parser exists, its output is critical.
return
self._resolve_waiters(event_name_upper, parsed_data)
# print(f"Dispatching event {event_name_upper} with data: {parsed_data} to {len(listeners)} listeners.")
for listener in listeners:
try:
# Inspect the listener to see how many arguments it expects
sig = inspect.signature(listener)
num_params = len(sig.parameters)
if num_params == 0: # Listener takes no arguments
await listener()
elif (
num_params == 1
): # Listener takes one argument (the parsed data or model)
await listener(parsed_data)
# elif num_params == 2 and event_name_upper == "MESSAGE_CREATE": # Special case for (client, message)
# await listener(self._client, parsed_data) # This might be too specific here
else:
# Fallback or error if signature doesn't match expected patterns
# For now, assume one arg is the most common for parsed data.
# Or, if you want to be strict:
print(
f"Warning: Listener {listener.__name__} for {event_name_upper} has an unhandled number of parameters ({num_params}). Skipping or attempting with one arg."
)
if num_params > 0: # Try with one arg if it takes any
await listener(parsed_data)
except Exception as e:
callback = self.on_dispatch_error
if callback is not None:
try:
await callback(event_name_upper, e, listener)
except Exception as hook_error:
print(f"Error in on_dispatch_error hook itself: {hook_error}")
else:
# Default error handling if no hook is set
print(
f"Error in event listener {listener.__name__} for {event_name_upper}: {e}"
)
if hasattr(self._client, "on_error"):
try:
await self._client.on_error(event_name_upper, e, listener)
except Exception as client_err_e:
print(f"Error in client.on_error itself: {client_err_e}")
await self._dispatch_to_listeners(event_name_upper, parsed_data)

View File

@ -3,6 +3,16 @@
Disagreement dispatches Gateway events to asynchronous callbacks. Handlers can be registered with `@client.event` or `client.on_event`.
Listeners may be removed later using `EventDispatcher.unregister(event_name, coro)`.
## Raw Events
Every Gateway event is also emitted with a `RAW_` prefix containing the unparsed payload. Raw events fire **before** any caching or parsing occurs.
```python
@client.on_event("RAW_MESSAGE_DELETE")
async def handle_raw_delete(payload: dict):
print("message deleted", payload["id"])
```
## PRESENCE_UPDATE

View File

@ -8,6 +8,7 @@ from disagreement.event_dispatcher import EventDispatcher
class DummyClient:
def __init__(self):
self.parsed = {}
self._messages = {"1": "cached"}
def parse_message(self, data):
self.parsed["message"] = True
@ -66,3 +67,27 @@ async def test_unregister_listener():
dispatcher.unregister("MESSAGE_CREATE", listener)
await dispatcher.dispatch("MESSAGE_CREATE", {"id": 1})
assert not called
@pytest.mark.asyncio
async def test_raw_event_dispatched_before_parsing():
client = DummyClient()
dispatcher = EventDispatcher(client)
events = {}
async def raw_listener(payload):
events["raw"] = payload
events["cache_before"] = client._messages.get("1")
async def delete_listener(_):
events["cache_after"] = client._messages.get("1")
dispatcher.register("RAW_MESSAGE_DELETE", raw_listener)
dispatcher.register("MESSAGE_DELETE", delete_listener)
await dispatcher.dispatch("MESSAGE_DELETE", {"id": "1"})
assert events["raw"]["id"] == "1"
assert events["cache_before"] == "cached"
assert events["cache_after"] is None