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.
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()(SLASHby 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'sOptionMapping(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:
- register the slash command with Discord (see registration), and
- 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(), usesReflectionHelperto scan forCommandimplementations 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 JDASlashCommandDataand 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
Taskso 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():
- Resolution — match the invoked name against a
CommandContainerinCommandManager. - 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.
- Usage parsing (
checkUsage) — detect whether a subcommand or subcommand group is being invoked and record the active subcommand on the context. - Option parsing
- Slash — Discord already parsed the arguments; map each
OptionMappingto aSlashCommandOption. - Message —
MessageCommandContexttokenizes the message and matches tokens against the declaredOptionData(resolving mentions to members/roles, parsing integers/booleans, etc.).
- Slash — Discord already parsed the arguments; map each
- Execution — call
command.execute(context). - Result handling — act on the returned
CommandExecutionResult: send success/error/warning responses, or, if aTaskwas returned, register it with theTaskManager.
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).