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.
5.8 KiB
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 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:
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 aContextto be routed onward (command or task flow).addGenericListener(Class<T>, Consumer<T>)— registers a factory that runs a side-effecting consumer and returnsnull, 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:
- Ignores messages authored by bots and webhook messages (returns
null). - Reads the configured prefix (
NETNEUTRALITY_BOT_PREFIX, default$). - If the trimmed message starts with the prefix →
MessageCommandContext(command flow). - Otherwise →
MessageTaskContext(task flow). This lets ordinary messages participate in the task system, e.g. as a reply that advances a step task.
Guild lifecycle (lambda flow)
GuildJoinEvent— resolvesGuildService.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
@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
CommandContextgoes toCommandProcessor.dispatch(), aTaskContextgoes toTaskManager.getInstance().handle(context). nullcontexts (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:
- Add an
addListener(SomeEvent.class, SomeContext::new)(oraddGenericListener(...)for a consumer-only side effect) to thestatic {}block. - If it should run the command or task flow, make the produced context extend
CommandContextorTaskContextrespectively.