diff --git a/.junie/plans/decouple-command-processor.md b/.junie/plans/decouple-command-processor.md new file mode 100644 index 0000000..32d3fc0 --- /dev/null +++ b/.junie/plans/decouple-command-processor.md @@ -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` 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 \ No newline at end of file diff --git a/src/main/java/me/bobert/netneutrality/context/command/MessageMenuContext.java b/src/main/java/me/bobert/netneutrality/context/command/MessageMenuContext.java index 5cbce78..7763e2b 100644 --- a/src/main/java/me/bobert/netneutrality/context/command/MessageMenuContext.java +++ b/src/main/java/me/bobert/netneutrality/context/command/MessageMenuContext.java @@ -21,6 +21,11 @@ import java.util.List; public class MessageMenuContext extends CommandContext implements InteractionContext { private final MessageContextInteractionEvent event; + @Override + public boolean isInteraction() { + return true; + } + public MessageMenuContext(MessageContextInteractionEvent event, SettingsProvider settings) { super(event.getJDA(), event.getGuild(), event.getChannel(), event.getMember(), event.getUser(), settings); this.event = event; diff --git a/src/main/java/me/bobert/netneutrality/context/command/SlashCommandContext.java b/src/main/java/me/bobert/netneutrality/context/command/SlashCommandContext.java index d5a381f..0ece84f 100644 --- a/src/main/java/me/bobert/netneutrality/context/command/SlashCommandContext.java +++ b/src/main/java/me/bobert/netneutrality/context/command/SlashCommandContext.java @@ -25,6 +25,16 @@ public class SlashCommandContext extends CommandContext implements InteractionCo this.event = event; } + @Override + public boolean isInteraction() { + return true; + } + + @Override + public boolean canSkipPermissionCheck() { + return true; + } + @Override public SlashCommandInteractionEvent getEvent() { return event; diff --git a/src/main/java/me/bobert/netneutrality/context/command/UserMenuContext.java b/src/main/java/me/bobert/netneutrality/context/command/UserMenuContext.java index 4f8722e..f5ecdf1 100644 --- a/src/main/java/me/bobert/netneutrality/context/command/UserMenuContext.java +++ b/src/main/java/me/bobert/netneutrality/context/command/UserMenuContext.java @@ -22,6 +22,11 @@ import java.util.List; public class UserMenuContext extends CommandContext implements InteractionContext { private final UserContextInteractionEvent event; + @Override + public boolean isInteraction() { + return true; + } + public UserMenuContext(UserContextInteractionEvent event, SettingsProvider settings) { super(event.getJDA(), event.getGuild(), (MessageChannel) event.getChannel(), event.getMember(), event.getUser(), settings); this.event = event; diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java b/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java index ee96122..0505ac9 100644 --- a/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandProcessor.java @@ -6,43 +6,55 @@ package me.bobert.netneutrality.core.command; */ import io.sentry.Sentry; -import me.bobert.netneutrality.context.command.SlashCommandContext; 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.InteractionContext; import me.bobert.netneutrality.exceptions.CommandExecutionException; import me.bobert.netneutrality.exceptions.GracefulContextError; import me.bobert.netneutrality.core.tasks.TaskManager; import me.bobert.netneutrality.shared.util.UserFunction; 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.Logger; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; 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 { private static final Logger logger = LogManager.getLogger(CommandProcessor.class); private final CommandContext context; private final CommandRegistry commands; private final TaskManager taskManager; + private final List preconditions; + private final UsageValidator usageValidator; + private final CommandResultHandler resultHandler; 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 preconditions, UsageValidator usageValidator, + CommandResultHandler resultHandler) { this.context = context; this.commands = Objects.requireNonNull(commands, "commands"); 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 defaultPreconditions() { + return List.of( + new GuildOnlyPrecondition(), + new SlashCommandOnlyPrecondition(), + new PermissionPrecondition() + ); } public void dispatch() { @@ -57,15 +69,11 @@ public class CommandProcessor { } context.setCommand(command); - if (command.isGuildOnly() && !context.isFromGuild()) { - context.replyEphemeral("This command is only available in a guild"); - context.deleteMessage(); - return; - } - if (command.isSlashCommandOnly() && !(context instanceof InteractionContext)) { - context.replyEphemeral("This command is only available as a slash command"); - return; + for (CommandPrecondition precondition : preconditions) { + if (!precondition.check(context, command)) { + return; + } } try { @@ -73,12 +81,7 @@ public class CommandProcessor { logger.info("Executing command {} on guild {}", command.getCommandName(), context.getGuild() == null ? "PrivateMessage" : context.getGuild().getId()); configureSentry(command); - if (command.getType() == Command.Type.SLASH && !canExecute(command)) { - context.reply("You are not powerful enough, scrub"); - return; - } - - if (command.getType() == Command.Type.SLASH && !checkUsage(container)) { + if (command.getType() == Command.Type.SLASH && !usageValidator.validate(context, container)) { return; } @@ -93,31 +96,7 @@ public class CommandProcessor { } CommandExecutionResult commandExecutionResult = command.execute(context); - 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); - } + resultHandler.handle(context, commandExecutionResult, taskManager); } catch (GracefulContextError exception) { logger.error(exception.getMessage()); context.replyEphemeral(exception.getMessage()); @@ -128,111 +107,6 @@ public class CommandProcessor { } } - private boolean checkUsage(CommandContainer container) { - List groups = container.getSubcommandGroups(); - List subcommands = container.getSubcommands(); - List 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 options) { - if (context instanceof InteractionContext) { - context.setCurrentCommandOptions(args, options); - return true; // slash commands are validated by discord - } - - List 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) { Sentry.configureScope(scope -> { scope.setTag("command", command.getCommandName()); diff --git a/src/main/java/me/bobert/netneutrality/core/command/CommandResultHandler.java b/src/main/java/me/bobert/netneutrality/core/command/CommandResultHandler.java new file mode 100644 index 0000000..3ca99fd --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/CommandResultHandler.java @@ -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); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/UsageValidator.java b/src/main/java/me/bobert/netneutrality/core/command/UsageValidator.java new file mode 100644 index 0000000..11836a3 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/UsageValidator.java @@ -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 groups = container.getSubcommandGroups(); + List subcommands = container.getSubcommands(); + List 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 options) { + if (context.isInteraction()) { + context.setCurrentCommandOptions(args, options); + return true; + } + + List 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()); + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/precondition/CommandPrecondition.java b/src/main/java/me/bobert/netneutrality/core/command/precondition/CommandPrecondition.java new file mode 100644 index 0000000..919bd4d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/precondition/CommandPrecondition.java @@ -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); +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/precondition/GuildOnlyPrecondition.java b/src/main/java/me/bobert/netneutrality/core/command/precondition/GuildOnlyPrecondition.java new file mode 100644 index 0000000..17ad189 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/precondition/GuildOnlyPrecondition.java @@ -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; + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/precondition/PermissionPrecondition.java b/src/main/java/me/bobert/netneutrality/core/command/precondition/PermissionPrecondition.java new file mode 100644 index 0000000..ffbdde4 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/precondition/PermissionPrecondition.java @@ -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; + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/command/precondition/SlashCommandOnlyPrecondition.java b/src/main/java/me/bobert/netneutrality/core/command/precondition/SlashCommandOnlyPrecondition.java new file mode 100644 index 0000000..6914381 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/core/command/precondition/SlashCommandOnlyPrecondition.java @@ -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; + } +} diff --git a/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java b/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java index 76abefa..d3907c1 100644 --- a/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java +++ b/src/main/java/me/bobert/netneutrality/core/context/CommandContext.java @@ -143,4 +143,12 @@ public abstract class CommandContext extends AbstractContext { public abstract String parseCommandName(); public abstract String getFullCommand(); + + public boolean isInteraction() { + return false; + } + + public boolean canSkipPermissionCheck() { + return false; + } }