170 lines
8.8 KiB
Markdown
170 lines
8.8 KiB
Markdown
# Commands
|
|
|
|
**Packages:** `me.bobert.netneutrality.framework.command` (framework),
|
|
`me.bobert.netneutrality.feature.*.command` (concrete commands)
|
|
|
|
A **Command** is what a user executes on Discord. It receives a
|
|
[`CommandContext`](contexts.md#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`](#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](#registration-lifecycle)), 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`](utilities-and-bootstrap.md#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](tasks.md) (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`](tasks.md).
|
|
|
|
Errors are surfaced through the exception hierarchy
|
|
([`CommandExecutionException`](utilities-and-bootstrap.md#exceptions), 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`](events.md), which calls `new CommandProcessor(context).dispatch()`.
|
|
|
|
## Command catalog
|
|
|
|
Concrete commands live under `me.bobert.netneutrality.feature.*.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 GitLab 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. |
|
|
| `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](persistence.md#service-layer).
|
|
|
|
**`PlayCommand` (music)** — extends `MusicCommand` (so it enforces guild-only + music role/channel).
|
|
It declares a single required `STRING` query option, delegates loading to
|
|
[`MusicManager`](music.md), and returns `CommandExecutionResult.none()` because the audio subsystem
|
|
produces the user-facing response asynchronously (often installing a
|
|
[`LoadPlaylistTask`](tasks.md#concrete-task-catalog) with choice buttons).
|