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

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 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.

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

@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 or TaskContext respectively.