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
+485
-156
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<CommandPrecondition> 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<CommandPrecondition> 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<CommandPrecondition> 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<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) {
|
||||
Sentry.configureScope(scope -> {
|
||||
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 getFullCommand();
|
||||
|
||||
public boolean isInteraction() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canSkipPermissionCheck() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user