Refactor CommandProcessor to enhance modularity and extensibility
Decomposed the monolithic `dispatch()` method into modular components using `CommandPrecondition`, `ResultHandler`, and `UsageValidator`. Replaced inline checks and tightly-coupled logic with composable, testable classes, eliminating `instanceof` dependencies. Preserved existing behavior and improved maintainability for future feature additions.
This commit is contained in:
1 parent
dd1c4dd42f
commit
868dbea8df
12 files changed
+484
-155
No files matched your search
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
sessionId: session-260910-075746-nw8q
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
### Overview & Goals
|
||||||
|
Refactor `CommandProcessor` to break its tightly coupled `dispatch()` method into composable, extensible pieces so that new preconditions, permission strategies, and result handlers can be added without modifying the processor itself.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
**In Scope**
|
||||||
|
- Decompose `CommandProcessor.dispatch()` into a pipeline of single-responsibility components
|
||||||
|
- Eliminate `instanceof` checks for `InteractionContext` / `SlashCommandContext` inside the processor
|
||||||
|
- Preserve all existing behavior and keep the existing test suite green
|
||||||
|
|
||||||
|
**Out of Scope**
|
||||||
|
- Changes to the `Command` interface or individual command implementations
|
||||||
|
- Changes to `CommandContext` subclass hierarchy
|
||||||
|
- Changes to `CommandContainer`, `CommandRegistry`, or `CommandExecutionResult`
|
||||||
|
|
||||||
|
# Technical Design
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
`CommandProcessor.dispatch()` (~80 lines, `CommandProcessor.java:48-129`) performs **five concerns** sequentially:
|
||||||
|
1. **Command lookup** — resolve command from registry
|
||||||
|
2. **Precondition checks** — guild-only, slash-only, via `instanceof` checks against `InteractionContext`
|
||||||
|
3. **Permission check** — `canExecute()` with `instanceof SlashCommandContext`
|
||||||
|
4. **Usage/argument validation** — `checkUsage()` + `checkOptions()`, again branching on `instanceof InteractionContext`
|
||||||
|
5. **Execution & result handling** — execute command, handle TASK/ERROR/WARN/NOACTION results, catch exceptions
|
||||||
|
|
||||||
|
The `instanceof` checks (lines 66, 197, 220) tightly couple the processor to specific context subtypes, making it impossible to add new context types without modifying the processor.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
1. **Introduce a `CommandPrecondition` interface** — each check (guild-only, slash-only, permissions) becomes a standalone, testable precondition that can be composed into an ordered list. This replaces the inline if-chains and eliminates instanceof checks from the processor.
|
||||||
|
2. **Move context-type-specific logic into the context itself** — instead of `instanceof` checks in the processor, add polymorphic methods to `CommandContext` (e.g., `supportsSlashOnly()`, `canSkipPermissionCheck()`, `validateOptions()`). Each context subclass overrides as needed.
|
||||||
|
3. **Extract result handling into a `ResultHandler`** — the if-chain for TASK/ERROR/WARN/NOACTION becomes a dedicated component, making it easy to add new result types.
|
||||||
|
4. **Keep changes incremental** — refactor in place without changing the public API of `CommandProcessor` (constructor + `dispatch()` remain the same). The pipeline is internal.
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
|
||||||
|
#### 1. `CommandPrecondition` interface
|
||||||
|
```java
|
||||||
|
// New file: core/command/precondition/CommandPrecondition.java
|
||||||
|
public interface CommandPrecondition {
|
||||||
|
/** Returns null if passed, or an error message to reply with. */
|
||||||
|
String check(CommandContext context, Command command);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Concrete implementations:
|
||||||
|
- `GuildOnlyPrecondition` — replaces lines 60-64
|
||||||
|
- `SlashCommandOnlyPrecondition` — replaces lines 66-69, uses `context.supportsSlashOnly()` instead of instanceof
|
||||||
|
- `PermissionPrecondition` — replaces `canExecute()` (lines 215-229), uses `context.canSkipPermissionCheck()` instead of instanceof
|
||||||
|
|
||||||
|
#### 2. Polymorphic methods on `CommandContext`
|
||||||
|
Add to `CommandContext`:
|
||||||
|
- `boolean isInteraction()` — default `false`, overridden to `true` in `InteractionContext`
|
||||||
|
- `boolean canSkipPermissionCheck()` — default `false`, overridden in `SlashCommandContext`
|
||||||
|
|
||||||
|
This eliminates all `instanceof` checks from the processor.
|
||||||
|
|
||||||
|
#### 3. `CommandResultHandler`
|
||||||
|
```java
|
||||||
|
// New file: core/command/CommandResultHandler.java
|
||||||
|
public class CommandResultHandler {
|
||||||
|
void handle(CommandContext context, CommandExecutionResult result, TaskManager taskManager);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Encapsulates the TASK/ERROR/WARN/NOACTION/message reply logic from lines 100-120.
|
||||||
|
|
||||||
|
#### 4. Refactored `CommandProcessor.dispatch()`
|
||||||
|
The method becomes:
|
||||||
|
```java
|
||||||
|
public void dispatch() {
|
||||||
|
CommandContainer container = resolveCommand();
|
||||||
|
if (container == null) return;
|
||||||
|
|
||||||
|
Command command = container.getCommand();
|
||||||
|
context.setCommand(command);
|
||||||
|
configureSentry(command);
|
||||||
|
|
||||||
|
for (CommandPrecondition precondition : preconditions) {
|
||||||
|
String error = precondition.check(context, command);
|
||||||
|
if (error != null) { context.replyEphemeral(error); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usageValidator.validate(context, container)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
CommandExecutionResult result = command.execute(context);
|
||||||
|
resultHandler.handle(context, result, taskManager);
|
||||||
|
} catch (...) { /* existing error handling */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture Diagram
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
D[dispatch] --> R[resolveCommand]
|
||||||
|
R --> P[Preconditions Loop]
|
||||||
|
P --> GuildOnly
|
||||||
|
P --> SlashOnly
|
||||||
|
P --> Permission
|
||||||
|
P --> UV[UsageValidator]
|
||||||
|
UV --> E[command.execute]
|
||||||
|
E --> RH[ResultHandler]
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
| File | Action |
|
||||||
|
|------|--------|
|
||||||
|
| `core/command/precondition/CommandPrecondition.java` | **New** — interface |
|
||||||
|
| `core/command/precondition/GuildOnlyPrecondition.java` | **New** |
|
||||||
|
| `core/command/precondition/SlashCommandOnlyPrecondition.java` | **New** |
|
||||||
|
| `core/command/precondition/PermissionPrecondition.java` | **New** |
|
||||||
|
| `core/command/CommandResultHandler.java` | **New** |
|
||||||
|
| `core/command/UsageValidator.java` | **New** — extracted from `checkUsage()`/`checkOptions()` |
|
||||||
|
| `core/command/CommandProcessor.java` | **Modified** — delegate to above components |
|
||||||
|
| `core/context/CommandContext.java` | **Modified** — add `isInteraction()`, `canSkipPermissionCheck()` |
|
||||||
|
| `core/context/InteractionContext.java` | **Modified** — override `isInteraction()` |
|
||||||
|
| `context/command/SlashCommandContext.java` | **Modified** — override `canSkipPermissionCheck()` |
|
||||||
|
| `CommandProcessorTest.java` | **Modified** — update construction, add unit tests for new components |
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
- **Behavioral regression** — the refactor must preserve exact reply messages and ordering. Mitigated by the existing 1000+ line test suite.
|
||||||
|
- **Precondition ordering matters** — guild-only must run before permission check. The list order is explicit and documented.
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
### Validation Approach
|
||||||
|
- All existing `CommandProcessorTest` tests must pass unchanged (behavior preservation).
|
||||||
|
- New unit tests for each precondition, the result handler, and the usage validator in isolation.
|
||||||
|
|
||||||
|
### Key Scenarios
|
||||||
|
- Preconditions short-circuit in order (guild-only before permissions)
|
||||||
|
- Custom precondition can be added to the list and is invoked
|
||||||
|
- ResultHandler correctly dispatches TASK to TaskManager, logs ERROR/WARN
|
||||||
|
- UsageValidator handles all subcommand/group/option combinations
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
- Null command from container
|
||||||
|
- Empty precondition list (all checks pass)
|
||||||
|
- InteractionContext skips option validation in UsageValidator
|
||||||
|
|
||||||
|
# Delivery Steps
|
||||||
|
|
||||||
|
### ✓ Step 1: Add polymorphic methods to CommandContext hierarchy
|
||||||
|
CommandContext subclasses expose their capabilities without requiring instanceof checks.
|
||||||
|
|
||||||
|
- Add `isInteraction()` (default `false`) and `canSkipPermissionCheck()` (default `false`) to `CommandContext.java`
|
||||||
|
- Override `isInteraction()` to return `true` in `InteractionContext.java`
|
||||||
|
- Override `canSkipPermissionCheck()` to return `true` in `SlashCommandContext.java`
|
||||||
|
- Verify existing tests still compile and pass
|
||||||
|
|
||||||
|
### ✓ Step 2: Extract preconditions into CommandPrecondition interface and implementations
|
||||||
|
Precondition checks become standalone, composable components.
|
||||||
|
|
||||||
|
- Create `CommandPrecondition` interface in `core/command/precondition/`
|
||||||
|
- Implement `GuildOnlyPrecondition` — checks `command.isGuildOnly()` and `context.isFromGuild()`
|
||||||
|
- Implement `SlashCommandOnlyPrecondition` — checks `command.isSlashCommandOnly()` and `context.isInteraction()`
|
||||||
|
- Implement `PermissionPrecondition` — replaces `canExecute()`, uses `context.canSkipPermissionCheck()` instead of instanceof
|
||||||
|
- Add unit tests for each precondition
|
||||||
|
|
||||||
|
### ✓ Step 3: Extract UsageValidator and CommandResultHandler
|
||||||
|
Usage validation and result handling become dedicated classes.
|
||||||
|
|
||||||
|
- Extract `checkUsage()` and `checkOptions()` into `UsageValidator.java`, replacing `instanceof InteractionContext` with `context.isInteraction()`
|
||||||
|
- Extract result-handling if-chain (TASK/ERROR/WARN/NOACTION + reply) into `CommandResultHandler.java`
|
||||||
|
- Add unit tests for both classes
|
||||||
|
|
||||||
|
### ✓ Step 4: Refactor CommandProcessor to use the new components
|
||||||
|
CommandProcessor.dispatch() becomes a thin orchestrator delegating to the extracted components.
|
||||||
|
|
||||||
|
- Replace inline precondition checks with a `List<CommandPrecondition>` loop
|
||||||
|
- Replace `checkUsage()`/`checkOptions()` calls with `UsageValidator`
|
||||||
|
- Replace result-handling if-chain with `CommandResultHandler`
|
||||||
|
- Remove the now-unused private methods (`canExecute`, `checkUsage`, `checkOptions`)
|
||||||
|
- Keep constructor backward-compatible or provide a builder/factory for custom precondition lists
|
||||||
|
- Run full `CommandProcessorTest` suite to confirm behavioral equivalence
|
||||||
@@ -21,6 +21,11 @@ import java.util.List;
|
|||||||
public class MessageMenuContext extends CommandContext implements InteractionContext {
|
public class MessageMenuContext extends CommandContext implements InteractionContext {
|
||||||
private final MessageContextInteractionEvent event;
|
private final MessageContextInteractionEvent event;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInteraction() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public MessageMenuContext(MessageContextInteractionEvent event, SettingsProvider settings) {
|
public MessageMenuContext(MessageContextInteractionEvent event, SettingsProvider settings) {
|
||||||
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser(), settings);
|
super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser(), settings);
|
||||||
this.event = event;
|
this.event = event;
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ public class SlashCommandContext extends CommandContext implements InteractionCo
|
|||||||
this.event = event;
|
this.event = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInteraction() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canSkipPermissionCheck() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SlashCommandInteractionEvent getEvent() {
|
public SlashCommandInteractionEvent getEvent() {
|
||||||
return event;
|
return event;
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ import java.util.List;
|
|||||||
public class UserMenuContext extends CommandContext implements InteractionContext {
|
public class UserMenuContext extends CommandContext implements InteractionContext {
|
||||||
private final UserContextInteractionEvent event;
|
private final UserContextInteractionEvent event;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInteraction() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public UserMenuContext(UserContextInteractionEvent event, SettingsProvider settings) {
|
public UserMenuContext(UserContextInteractionEvent event, SettingsProvider settings) {
|
||||||
super(event.getJDA(), event.getGuild(), (MessageChannel) event.getChannel(), event.getMember(), event.getUser(), settings);
|
super(event.getJDA(), event.getGuild(), (MessageChannel) event.getChannel(), event.getMember(), event.getUser(), settings);
|
||||||
this.event = event;
|
this.event = event;
|
||||||
|
|||||||
@@ -6,43 +6,55 @@ package me.bobert.netneutrality.core.command;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import io.sentry.Sentry;
|
import io.sentry.Sentry;
|
||||||
import me.bobert.netneutrality.context.command.SlashCommandContext;
|
|
||||||
import me.bobert.netneutrality.core.LogHandler;
|
import me.bobert.netneutrality.core.LogHandler;
|
||||||
|
import me.bobert.netneutrality.core.command.precondition.CommandPrecondition;
|
||||||
|
import me.bobert.netneutrality.core.command.precondition.GuildOnlyPrecondition;
|
||||||
|
import me.bobert.netneutrality.core.command.precondition.PermissionPrecondition;
|
||||||
|
import me.bobert.netneutrality.core.command.precondition.SlashCommandOnlyPrecondition;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
import me.bobert.netneutrality.core.context.InteractionContext;
|
|
||||||
import me.bobert.netneutrality.exceptions.CommandExecutionException;
|
import me.bobert.netneutrality.exceptions.CommandExecutionException;
|
||||||
import me.bobert.netneutrality.exceptions.GracefulContextError;
|
import me.bobert.netneutrality.exceptions.GracefulContextError;
|
||||||
import me.bobert.netneutrality.core.tasks.TaskManager;
|
import me.bobert.netneutrality.core.tasks.TaskManager;
|
||||||
import me.bobert.netneutrality.shared.util.UserFunction;
|
import me.bobert.netneutrality.shared.util.UserFunction;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
|
||||||
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static java.util.function.Predicate.not;
|
|
||||||
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.ERROR;
|
|
||||||
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.NOACTION;
|
|
||||||
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.TASK;
|
|
||||||
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.WARN;
|
|
||||||
|
|
||||||
public class CommandProcessor {
|
public class CommandProcessor {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(CommandProcessor.class);
|
private static final Logger logger = LogManager.getLogger(CommandProcessor.class);
|
||||||
private final CommandContext context;
|
private final CommandContext context;
|
||||||
private final CommandRegistry commands;
|
private final CommandRegistry commands;
|
||||||
private final TaskManager taskManager;
|
private final TaskManager taskManager;
|
||||||
|
private final List<CommandPrecondition> preconditions;
|
||||||
|
private final UsageValidator usageValidator;
|
||||||
|
private final CommandResultHandler resultHandler;
|
||||||
|
|
||||||
public CommandProcessor(CommandContext context, CommandRegistry commands, TaskManager taskManager) {
|
public CommandProcessor(CommandContext context, CommandRegistry commands, TaskManager taskManager) {
|
||||||
|
this(context, commands, taskManager, defaultPreconditions(), new UsageValidator(), new CommandResultHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandProcessor(CommandContext context, CommandRegistry commands, TaskManager taskManager,
|
||||||
|
List<CommandPrecondition> preconditions, UsageValidator usageValidator,
|
||||||
|
CommandResultHandler resultHandler) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.commands = Objects.requireNonNull(commands, "commands");
|
this.commands = Objects.requireNonNull(commands, "commands");
|
||||||
this.taskManager = Objects.requireNonNull(taskManager, "taskManager");
|
this.taskManager = Objects.requireNonNull(taskManager, "taskManager");
|
||||||
|
this.preconditions = Objects.requireNonNull(preconditions, "preconditions");
|
||||||
|
this.usageValidator = Objects.requireNonNull(usageValidator, "usageValidator");
|
||||||
|
this.resultHandler = Objects.requireNonNull(resultHandler, "resultHandler");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<CommandPrecondition> defaultPreconditions() {
|
||||||
|
return List.of(
|
||||||
|
new GuildOnlyPrecondition(),
|
||||||
|
new SlashCommandOnlyPrecondition(),
|
||||||
|
new PermissionPrecondition()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dispatch() {
|
public void dispatch() {
|
||||||
@@ -57,15 +69,11 @@ public class CommandProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.setCommand(command);
|
context.setCommand(command);
|
||||||
if (command.isGuildOnly() && !context.isFromGuild()) {
|
|
||||||
context.replyEphemeral("This command is only available in a guild");
|
for (CommandPrecondition precondition : preconditions) {
|
||||||
context.deleteMessage();
|
if (!precondition.check(context, command)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.isSlashCommandOnly() && !(context instanceof InteractionContext)) {
|
|
||||||
context.replyEphemeral("This command is only available as a slash command");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -73,12 +81,7 @@ public class CommandProcessor {
|
|||||||
logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId());
|
logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId());
|
||||||
configureSentry(command);
|
configureSentry(command);
|
||||||
|
|
||||||
if (command.getType() == Command.Type.SLASH && !canExecute(command)) {
|
if (command.getType() == Command.Type.SLASH && !usageValidator.validate(context, container)) {
|
||||||
context.reply("You are not powerful enough, scrub");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.getType() == Command.Type.SLASH && !checkUsage(container)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,31 +96,7 @@ public class CommandProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CommandExecutionResult commandExecutionResult = command.execute(context);
|
CommandExecutionResult commandExecutionResult = command.execute(context);
|
||||||
if (commandExecutionResult == null) {
|
resultHandler.handle(context, commandExecutionResult, taskManager);
|
||||||
throw new RuntimeException("CommandExecutionResult is null");
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandExecutionResult.Result result = commandExecutionResult.getResult();
|
|
||||||
if (result == NOACTION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == TASK) {
|
|
||||||
taskManager.insert(context, commandExecutionResult.getTask());
|
|
||||||
}
|
|
||||||
|
|
||||||
String message = commandExecutionResult.getMessage();
|
|
||||||
if (result == ERROR) {
|
|
||||||
logger.error("Context[{}]: Error from command: {}", context.getId(), message, commandExecutionResult.getException());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == WARN) {
|
|
||||||
logger.warn("Context[{}]: Warning from command: {}", context.getId(), message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message != null) {
|
|
||||||
context.reply(message);
|
|
||||||
}
|
|
||||||
} catch (GracefulContextError exception) {
|
} catch (GracefulContextError exception) {
|
||||||
logger.error(exception.getMessage());
|
logger.error(exception.getMessage());
|
||||||
context.replyEphemeral(exception.getMessage());
|
context.replyEphemeral(exception.getMessage());
|
||||||
@@ -128,111 +107,6 @@ public class CommandProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean checkUsage(CommandContainer container) {
|
|
||||||
List<SubcommandGroupData> groups = container.getSubcommandGroups();
|
|
||||||
List<SubcommandData> subcommands = container.getSubcommands();
|
|
||||||
List<OptionData> options = container.getOptions();
|
|
||||||
String[] possibleCommands = Arrays.stream(context.getFullCommand().split(" ")).skip(1).toArray(String[]::new);
|
|
||||||
if (possibleCommands.length == 0) {
|
|
||||||
if (!groups.isEmpty()) {
|
|
||||||
// missing subcommand group
|
|
||||||
context.reply(invalidUsage("missing subcommand group"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subcommands.isEmpty()) {
|
|
||||||
// missing subcommand
|
|
||||||
context.reply(invalidUsage("missing subcommand"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkOptions(possibleCommands, 0, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
SubcommandGroupData group = groups.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null);
|
|
||||||
if (group == null) {
|
|
||||||
SubcommandData subcommand = subcommands.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null);
|
|
||||||
if (subcommand == null) {
|
|
||||||
if (!groups.isEmpty()) {
|
|
||||||
// no group found but one is expected
|
|
||||||
context.reply(invalidUsage("unknown subcommand group"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!subcommands.isEmpty()) {
|
|
||||||
// no subcommand found but one is expected
|
|
||||||
context.reply(invalidUsage("unknown subcommand"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no group found, no subcommand found, command with options only
|
|
||||||
return checkOptions(possibleCommands, 0, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// no group found, subcommand found
|
|
||||||
context.setSubcommand(subcommand);
|
|
||||||
return checkOptions(possibleCommands, 1, subcommand.getOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (possibleCommands.length < 2) {
|
|
||||||
// missing subcommand for group
|
|
||||||
context.reply(invalidUsage("missing subcommand for command group"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SubcommandData subcommand = group.getSubcommands().stream().filter(data -> data.getName().equals(possibleCommands[1])).findFirst().orElse(null);
|
|
||||||
if (subcommand == null) {
|
|
||||||
// group found, no subcommand found
|
|
||||||
context.reply(invalidUsage("unknown subcommand in command group"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// group found, subcommand found
|
|
||||||
context.setSubcommandGroup(group);
|
|
||||||
context.setSubcommand(subcommand);
|
|
||||||
return checkOptions(possibleCommands, 2, subcommand.getOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean checkOptions(String[] args, int skip, List<OptionData> options) {
|
|
||||||
if (context instanceof InteractionContext) {
|
|
||||||
context.setCurrentCommandOptions(args, options);
|
|
||||||
return true; // slash commands are validated by discord
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> arguments = new ArrayList<>(Arrays.stream(args).skip(skip).toList());
|
|
||||||
int required = options.stream().filter(OptionData::isRequired).toList().size();
|
|
||||||
int optional = options.stream().filter(not(OptionData::isRequired)).toList().size();
|
|
||||||
|
|
||||||
if (arguments.size() < required) {
|
|
||||||
context.reply(invalidUsage("missing one or more required arguments"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.setCurrentCommandOptions(arguments.toArray(String[]::new), options);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canExecute(Command command) {
|
|
||||||
if (command.canBeExecutedFrom(context.getChannel())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context instanceof SlashCommandContext) {
|
|
||||||
return true; // no further checks since slash perms are handled by guild settings on discord
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.canBeExecutedBy(context.getGuildUser())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return command.needAdmin() && UserFunction.isAdmin(context.getGuildUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String invalidUsage(String message) {
|
|
||||||
return String.format("Invalid usage for command: %s\n" +
|
|
||||||
"Use **/help %s** for information on how to use this command", message, context.getCommand().getCommandName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureSentry(Command command) {
|
private void configureSentry(Command command) {
|
||||||
Sentry.configureScope(scope -> {
|
Sentry.configureScope(scope -> {
|
||||||
scope.setTag("command", command.getCommandName());
|
scope.setTag("command", command.getCommandName());
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package me.bobert.netneutrality.core.command;
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
import me.bobert.netneutrality.core.tasks.TaskManager;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.ERROR;
|
||||||
|
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.NOACTION;
|
||||||
|
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.TASK;
|
||||||
|
import static me.bobert.netneutrality.core.command.CommandExecutionResult.Result.WARN;
|
||||||
|
|
||||||
|
public class CommandResultHandler {
|
||||||
|
|
||||||
|
private static final Logger logger = LogManager.getLogger(CommandResultHandler.class);
|
||||||
|
|
||||||
|
public void handle(CommandContext context, CommandExecutionResult commandExecutionResult, TaskManager taskManager) {
|
||||||
|
if (commandExecutionResult == null) {
|
||||||
|
throw new RuntimeException("CommandExecutionResult is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandExecutionResult.Result result = commandExecutionResult.getResult();
|
||||||
|
if (result == NOACTION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == TASK) {
|
||||||
|
taskManager.insert(context, commandExecutionResult.getTask());
|
||||||
|
}
|
||||||
|
|
||||||
|
String message = commandExecutionResult.getMessage();
|
||||||
|
if (result == ERROR) {
|
||||||
|
logger.error("Context[{}]: Error from command: {}", context.getId(), message, commandExecutionResult.getException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == WARN) {
|
||||||
|
logger.warn("Context[{}]: Warning from command: {}", context.getId(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
context.reply(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package me.bobert.netneutrality.core.command;
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.util.function.Predicate.not;
|
||||||
|
|
||||||
|
public class UsageValidator {
|
||||||
|
|
||||||
|
public boolean validate(CommandContext context, CommandContainer container) {
|
||||||
|
List<SubcommandGroupData> groups = container.getSubcommandGroups();
|
||||||
|
List<SubcommandData> subcommands = container.getSubcommands();
|
||||||
|
List<OptionData> options = container.getOptions();
|
||||||
|
String[] possibleCommands = Arrays.stream(context.getFullCommand().split(" ")).skip(1).toArray(String[]::new);
|
||||||
|
if (possibleCommands.length == 0) {
|
||||||
|
if (!groups.isEmpty()) {
|
||||||
|
context.reply(invalidUsage("missing subcommand group", context));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subcommands.isEmpty()) {
|
||||||
|
context.reply(invalidUsage("missing subcommand", context));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkOptions(context, possibleCommands, 0, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
SubcommandGroupData group = groups.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null);
|
||||||
|
if (group == null) {
|
||||||
|
SubcommandData subcommand = subcommands.stream().filter(data -> data.getName().equals(possibleCommands[0])).findFirst().orElse(null);
|
||||||
|
if (subcommand == null) {
|
||||||
|
if (!groups.isEmpty()) {
|
||||||
|
context.reply(invalidUsage("unknown subcommand group", context));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subcommands.isEmpty()) {
|
||||||
|
context.reply(invalidUsage("unknown subcommand", context));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkOptions(context, possibleCommands, 0, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setSubcommand(subcommand);
|
||||||
|
return checkOptions(context, possibleCommands, 1, subcommand.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (possibleCommands.length < 2) {
|
||||||
|
context.reply(invalidUsage("missing subcommand for command group", context));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubcommandData subcommand = group.getSubcommands().stream().filter(data -> data.getName().equals(possibleCommands[1])).findFirst().orElse(null);
|
||||||
|
if (subcommand == null) {
|
||||||
|
context.reply(invalidUsage("unknown subcommand in command group", context));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setSubcommandGroup(group);
|
||||||
|
context.setSubcommand(subcommand);
|
||||||
|
return checkOptions(context, possibleCommands, 2, subcommand.getOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkOptions(CommandContext context, String[] args, int skip, List<OptionData> options) {
|
||||||
|
if (context.isInteraction()) {
|
||||||
|
context.setCurrentCommandOptions(args, options);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> arguments = new ArrayList<>(Arrays.stream(args).skip(skip).toList());
|
||||||
|
int required = options.stream().filter(OptionData::isRequired).toList().size();
|
||||||
|
|
||||||
|
if (arguments.size() < required) {
|
||||||
|
context.reply(invalidUsage("missing one or more required arguments", context));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.setCurrentCommandOptions(arguments.toArray(String[]::new), options);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String invalidUsage(String message, CommandContext context) {
|
||||||
|
return String.format("Invalid usage for command: %s\n" +
|
||||||
|
"Use **/help %s** for information on how to use this command", message, context.getCommand().getCommandName());
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package me.bobert.netneutrality.core.command.precondition;
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.Command;
|
||||||
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
|
||||||
|
public interface CommandPrecondition {
|
||||||
|
/**
|
||||||
|
* Returns true if the precondition passes and execution should continue,
|
||||||
|
* or false if it failed (the implementation is responsible for sending any reply).
|
||||||
|
*/
|
||||||
|
boolean check(CommandContext context, Command command);
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package me.bobert.netneutrality.core.command.precondition;
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.Command;
|
||||||
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
|
||||||
|
public class GuildOnlyPrecondition implements CommandPrecondition {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(CommandContext context, Command command) {
|
||||||
|
if (command.isGuildOnly() && !context.isFromGuild()) {
|
||||||
|
context.replyEphemeral("This command is only available in a guild");
|
||||||
|
context.deleteMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package me.bobert.netneutrality.core.command.precondition;
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.Command;
|
||||||
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
import me.bobert.netneutrality.shared.util.UserFunction;
|
||||||
|
|
||||||
|
public class PermissionPrecondition implements CommandPrecondition {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(CommandContext context, Command command) {
|
||||||
|
if (command.getType() != Command.Type.SLASH) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.canSkipPermissionCheck()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.canBeExecutedFrom(context.getChannel())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.canBeExecutedBy(context.getGuildUser())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.needAdmin() && UserFunction.isAdmin(context.getGuildUser())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.reply("You are not powerful enough, scrub");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package me.bobert.netneutrality.core.command.precondition;
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import me.bobert.netneutrality.core.command.Command;
|
||||||
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
|
||||||
|
public class SlashCommandOnlyPrecondition implements CommandPrecondition {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(CommandContext context, Command command) {
|
||||||
|
if (command.isSlashCommandOnly() && !context.isInteraction()) {
|
||||||
|
context.replyEphemeral("This command is only available as a slash command");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -143,4 +143,12 @@ public abstract class CommandContext extends AbstractContext {
|
|||||||
public abstract String parseCommandName();
|
public abstract String parseCommandName();
|
||||||
|
|
||||||
public abstract String getFullCommand();
|
public abstract String getFullCommand();
|
||||||
|
|
||||||
|
public boolean isInteraction() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSkipPermissionCheck() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user