Files
netneutrality/docs/events.md
anthorob 142cc77ec8 **Add playlist metadata support to queue and events**
Enhanced queueing functionality to include playlist metadata, such as name, identifier, and track count. Updated event payloads to handle playlist information and introduced a new `playlist_added` event for streamlined notifications. Adjusted tests and documentation to reflect these changes.
2026-10-28 16:34:38 -04:00

118 lines
5.8 KiB
Markdown

# Event Handling & Routing
**Package:** `me.bobert.netneutrality.hooks`
**Primary class:** `EventsHandler`
`EventsHandler` is the single JDA `EventListener` registered on the `JDA` instance
(`jda.addEventListener(new EventsHandler())` in `App.startup()`). It is the funnel through
which **every** gateway event passes. Its job is to translate a raw JDA event into a
[Context](contexts.md) and route that context to the correct subsystem.
## Listener registry
Listeners are held in a static, thread-safe map keyed by the JDA event class:
```java
private static final Map<Class<? extends Event>, Function<? extends Event, Context>> listeners
= new ConcurrentHashMap<>();
```
Each entry is a **factory function**: given an event of that type, it returns a `Context` (or `null`).
The map is populated once in a `static {}` block.
### `addListener` vs `addGenericListener`
- `addListener(Class<T>, Function<? super T, Context>)` — registers a factory that produces a
`Context` to be routed onward (command or task flow).
- `addGenericListener(Class<T>, Consumer<T>)` — registers a factory that runs a side-effecting
consumer and returns `null`, so nothing is routed onward. This is the **lambda flow** for simple
events.
## Registered events
| JDA event | Produces | Flow |
|---|---|---|
| `MessageReceivedEvent` (prefix match) | `MessageCommandContext` | Command |
| `MessageReceivedEvent` (no prefix) | `MessageTaskContext` | Task |
| `SlashCommandInteractionEvent` | `SlashCommandContext` | Command |
| `UserContextInteractionEvent` | `UserMenuContext` | Command |
| `MessageContextInteractionEvent` | `MessageMenuContext` | Command |
| `ButtonInteractionEvent` | `ButtonTaskContext` | Task |
| `ModalInteractionEvent` | `ModalTaskContext` | Task |
| `MessageReactionAddEvent` | `EmojiTaskContext` | Task |
| `GuildJoinEvent` | — (consumer) | Lambda |
| `GuildLeaveEvent` | — (consumer) | Lambda |
### Message routing detail
For `MessageReceivedEvent` the factory:
1. Ignores messages authored by bots and webhook messages (returns `null`).
2. Reads the configured prefix (`NETNEUTRALITY_BOT_PREFIX`, default `$`).
3. If the trimmed message starts with the prefix → **`MessageCommandContext`** (command flow).
4. Otherwise → **`MessageTaskContext`** (task flow). This lets ordinary messages participate in the
task system, e.g. as a reply that advances a [step task](tasks.md#33-step-tasks).
### Guild lifecycle (lambda flow)
- `GuildJoinEvent` — resolves `GuildService.get(guild)`; if debug logging is enabled for that guild,
sends a notice to the debug channel.
- `GuildLeaveEvent` — `GuildService.get(guild).deleteGuild()` removes the guild's stored data.
## Dispatch algorithm
```java
@Override
public void onEvent(@Nonnull GenericEvent event) {
handleEvent((Event) event);
}
private void handleEvent(Event event) {
Class<?> eventClass = event.getClass();
while (eventClass != null) {
if (listeners.containsKey(eventClass)) {
Context context = listeners.get(eventClass).apply(event);
if (context instanceof CommandContext c) handleCommand(c);
if (context instanceof TaskContext t) handleTask(t);
break;
}
eventClass = eventClass.getSuperclass(); // walk up the class hierarchy
}
}
```
Key points:
- **Superclass walk** — if the exact event class is not registered, the handler walks up the
superclass chain until it finds a match. This means a listener registered for a base event type
also catches subclasses.
- **Routing by context type** — the resulting object is routed purely by its runtime type:
a `CommandContext` goes to `CommandProcessor.dispatch()`, a `TaskContext` goes to
`TaskManager.getInstance().handle(context)`.
- **`null` contexts** (bot messages, consumer-only events) are simply not routed anywhere.
## Webhook music events
`MusicEvent` instances are delivered by `WebhookDispatcher`. Every event carries `type`, `guildId`,
`timestamp` and `seq`; the payloads of the queue and player events are:
| Type | Payload |
|---|---|
| `QUEUE_UPDATED` | `size` (int), `change` (`added`/`playlist_added`/`removed`/`moved`/`cleared`/`shuffled`), `items` (list). Each item has `index` (0-based position, before the mutation for removals), `identifier`, `title`, `author`, `duration_ms`, `uri`, `artwork_url`, `requester_id` and `playlist` (`{name, identifier, track_count}` or `null` when the track was not queued from a playlist). `moved` items also carry `fromIndex` and `toIndex`. `cleared` and `shuffled` have an empty `items` (reload the queue on `shuffled`). `playlist_added` is emitted once per playlist load (instead of one `added` event per track), lists only the tracks actually added (duplicates are skipped) and carries a top-level `playlist` object (`name`, `identifier`, `track_count`, the number actually added). |
| `QUEUE_SHUFFLED` | `size` |
| `TRACK_ERROR` | `track` (`identifier`, `title`, `author`, `length`, `uri`, `artworkUrl`), `message`, `severity` |
| `TRACK_STUCK` | `track` (same fields as above), `thresholdMs` |
| `PLAYER_POSITION` | `position` (ms), `timestamp` (node time, epoch ms), `connected`, `ping` (ms). Sent from Lavalink `PlayerUpdateEvent`, at most once every 5 seconds per guild and only while a track is playing. |
Consumers must handle the `playlist_added` change value and the per-item `playlist` field. The REST queue listing (`GET /music/{guildId}/queue`) also reports `playlist` on every entry.
## Extending the event system
To handle a new event type:
1. Add an `addListener(SomeEvent.class, SomeContext::new)` (or `addGenericListener(...)` for a
consumer-only side effect) to the `static {}` block.
2. If it should run the command or task flow, make the produced context extend
[`CommandContext`](contexts.md#commandcontext) or [`TaskContext`](contexts.md#taskcontext)
respectively.