Files
netneutrality/docs/commands.md
anthorob d97c4c6da2 **Add autoplay support for seamless music playback**
Introduced autoplay functionality to keep music playing when the queue ends. Added new `/autoplay` API endpoints, command handling, and service implementations. Enhanced `TrackScheduler`, queue operations, and testing to support autoplay behavior. Integrated YouTube radio mix as the initial provider.
2026-10-31 16:13:04 -04:00

9.0 KiB

Commands

Packages: me.bobert.netneutrality.core.command (framework), me.bobert.netneutrality.command.* (concrete commands)

A Command is what a user executes on Discord. It receives a CommandContext and runs logic based on it. There are two primary kinds:

  • Slash commands — first-class Discord application commands. Their structure (options, subcommands, groups) has rules defined by Discord and must be configured procedurally via CommandOptionsBuilder.
  • Message (prefix) commands — a plain message beginning with the configured prefix (NETNEUTRALITY_BOT_PREFIX, default $).

A core goal of the framework is that both kinds are authored the same way: a command declares its metadata and options once, and the framework adapts it to either interaction style. Most of that adaptation logic lives in CommandProcessor.

The Command contract

Command (interface) defines:

  • Metadata — getCommandName(), getDescription(), getCategory() (CommandCategory).
  • Type — getType() (SLASH by default).
  • Permissions — needAdmin(), canBeExecutedBy(...), guild-only / slash-only constraints.
  • Option declaration — registerCommandOptions(CommandOptionsBuilder) (procedural option setup).
  • Execution — execute(CommandContext) : CommandExecutionResult.

Base implementations (core.command.impl)

Class Adds
AbstractCommand Default Command implementation; type defaults to SLASH; basic permission logic.
AdminCommand Sets needAdmin() = true and category ADMINISTRATION.
MusicCommand Guild-only; verifies the guild's music role and music channel before execution.
MessageMenuCommand Discord message context-menu command; skips option registration; wraps the target Message.
UserMenuCommand Discord user context-menu command; skips option registration; wraps the target User.

Unified options

To let one command definition serve both slash and message input, values are read through a wrapper interface:

  • CommandOption (interface) — uniform accessors: getAsString(), getAsInt(), getAsMember(), getAsBoolean(), … .
  • SlashCommandOption — wraps JDA's OptionMapping (Discord already parsed the value).
  • MessageCommandOption — wraps a value parsed manually from the message text.

Procedural option configuration — CommandOptionsBuilder

CommandOptionsBuilder implements JDA's SlashCommandData and acts as a proxy over JDA's internal CommandDataImpl. A command builds its argument surface in registerCommandOptions(builder) by adding options, subcommands, and subcommand groups. The same declaration is used both to:

  1. register the slash command with Discord (see registration), and
  2. drive the message-command parser (which tries to match tokens in the message against the declared OptionData).

Command management

CommandManager

Responsible for discovery and registration.

  • Discovery — at init(), uses ReflectionHelper to scan for Command implementations and instantiate them.
  • Storage — commands are kept in a ConcurrentHashMap<String, CommandContainer> keyed by command name.
  • Slash registration — registerSlashCommands(JDA) converts each command's declaration into JDA SlashCommandData and pushes it to Discord's API.

CommandContainer

Wraps a Command instance together with its JDA metadata (CommandDataImpl). It also builds the "usage" string shown by the help command.

CommandCategory

Enum grouping commands for help/display purposes (e.g. ADMINISTRATION, UTILITY, MUSIC, CRYPTO, and an unlisted/hidden group for owner/debug commands).

CommandExecutionResult

The value returned by execute(...). It encodes the outcome and any response payload:

  • success(...) — normal success (optionally with a message/embed to send).
  • error(...) / warning(...) — failure/advisory outcomes with a message.
  • none() — nothing to send synchronously (the command handles its own async response, e.g. music).
  • May also carry a follow-up Task so a command can install an interactive task (e.g. buttons) as part of its result.

The dispatch flow — CommandProcessor

CommandProcessor is constructed with the CommandContext and driven by dispatch():

  1. Resolution — match the invoked name against a CommandContainer in CommandManager.
  2. Validation
    • command enabled?
    • guild-only / slash-only constraints satisfied?
    • permissions — canBeExecutedBy(...) and, for admin commands, needAdmin() verified against the bot owner (NETNEUTRALITY_BOT_OWNER) and the member's guild-admin status.
  3. Usage parsing (checkUsage) — detect whether a subcommand or subcommand group is being invoked and record the active subcommand on the context.
  4. Option parsing
    • Slash — Discord already parsed the arguments; map each OptionMapping to a SlashCommandOption.
    • Message — MessageCommandContext tokenizes the message and matches tokens against the declared OptionData (resolving mentions to members/roles, parsing integers/booleans, etc.).
  5. Execution — call command.execute(context).
  6. Result handling — act on the returned CommandExecutionResult: send success/error/warning responses, or, if a Task was returned, register it with the TaskManager.

Errors are surfaced through the exception hierarchy (CommandExecutionException, and GracefulContextError for user-facing, non-logged failures).

Registration lifecycle

App.startup()
   ├─ CommandManager.init()               // reflective discovery → ConcurrentHashMap
   └─ CommandManager.registerSlashCommands(jda)  // push SlashCommandData to Discord

At runtime, an incoming command event becomes a CommandContext in the EventsHandler, which calls new CommandProcessor(context).dispatch().

Command catalog

Concrete commands live under me.bobert.netneutrality.command.*, grouped by feature package:

Command Package Type Description
BlacklistCommand admin Slash Manage the server's word blacklist.
ReactionCommand admin Slash Manage emoji-reaction roles on the server.
HelpCommand basic Slash List every command with its usage.
PingCommand basic Slash Reply with the gateway ping.
ConfigureCommand bot Slash Configure the bot for the server (subcommands).
CryptoPriceCommand crypto Slash Fetch cryptocurrency prices.
ReportIssueCommand git Slash Report a bug (opens the Gitea report flow).
PlayCommand music Slash Search/queue and start playback.
QueueCommand / ViewQueueCommand music Slash View/manage the queue.
SkipCommand / VoteSkipCommand music Slash Skip (or vote-skip) the current track.
ClearQueueCommand / ShuffleCommand music Slash Clear / shuffle the queue.
JoinCommand / LeaveCommand music Slash Join / leave the voice channel.
CurrentlyPlayingCommand music Slash Show the currently playing track.
ResumeCommand / StopCommand / VolumeCommand music Slash Playback controls.
AutoplayCommand music Slash Toggle autoplay: queues the YouTube radio mix of the last track so music keeps playing.
BitconnectCommand music Slash Novelty music command.
CreateEmojiReaction / ModifyEmojiReaction react Message menu Create/modify emoji-role mappings from a message.
DatabaseCommand, PurgeCommand, ShutdownCommand, JoinClanCommand, TestCommand, SlashCommandTest, TrackStuckTest custom Mixed Owner/debug and personal utilities (the custom package is gitignored per-deployment).

Representative commands

PingCommand (simplest slash command) — extends AbstractCommand, overrides execute to return CommandExecutionResult.success(...) containing the JDA gateway ping. It illustrates the minimal execution path (no options, synchronous response).

ConfigureCommand (complex slash command) — uses registerCommandOptions to declare several SubcommandData (e.g. music-volume, error-logging). In execute, it switches on context.getSubcommand() and reads typed values via context.getOption(name, resolver), then persists them through the guild settings service.

PlayCommand (music) — extends MusicCommand (so it enforces guild-only + music role/channel). It declares a single required STRING query option, delegates loading to MusicManager, and returns CommandExecutionResult.none() because the audio subsystem produces the user-facing response asynchronously (often installing a LoadPlaylistTask with choice buttons).