Files
netneutrality/docs/events.md
T
anthorob 87ba09b001 **Improve event handling with new music and queue updates**
Added new `MusicEvent` types, including `TRACK_ERROR`, `TRACK_STUCK`, and `PLAYER_POSITION`, providing enhanced diagnostics and feedback. Improved queue mutation notifications by introducing granular events like `added`, `removed`, `moved`, `cleared`, and `shuffled`. Updated tests and documentation to reflect the changes, ensuring consistency and clarity across the event system.
2026-10-28 03:58:19 -04:00

5.3 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/removed/moved/cleared/shuffled), items (list). Each item has index (0-based position, before the mutation for removals), identifier, title, uri, requesterId. moved items also carry fromIndex and toIndex. cleared and shuffled have an empty items (reload the queue on shuffled).
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.

size in QUEUE_UPDATED is unchanged, so existing consumers keep working.

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.