diff --git a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java index 7e7d041..acc91df 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -5,6 +5,7 @@ import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.api.arguments.TabCompleter; import fr.traqueur.commands.api.exceptions.ArgumentIncorrectException; +import fr.traqueur.commands.api.exceptions.CommandRegistrationException; import fr.traqueur.commands.api.exceptions.TypeArgumentNotExistException; import fr.traqueur.commands.api.logging.Logger; import fr.traqueur.commands.api.logging.MessageHandler; @@ -150,7 +151,7 @@ public void registerCommand(Command command) { this.registerSubCommands(alias, command.getSubcommands()); } } catch(TypeArgumentNotExistException e) { - throw new RuntimeException(e); + throw new CommandRegistrationException("Failed to register command: " + command.getClass().getSimpleName(), e); } } @@ -299,8 +300,8 @@ private void registerSubCommands(String parentLabel, List> subcomma return; } for (Command subcommand : subcommands) { + // getAliases() already returns [name, ...aliases], so no need to add the name again List aliasesSub = new ArrayList<>(subcommand.getAliases()); - aliasesSub.add(subcommand.getName()); for (String aliasSub : aliasesSub) { this.addCommand(subcommand, parentLabel + "." + aliasSub); this.registerSubCommands(parentLabel + "." + aliasSub, subcommand.getSubcommands()); @@ -318,8 +319,8 @@ private void unregisterSubCommands(String parentLabel, List> subcom return; } for (Command subcommand : subcommandsList) { + // getAliases() already returns [name, ...aliases], so no need to add the name again List aliasesSub = new ArrayList<>(subcommand.getAliases()); - aliasesSub.add(subcommand.getName()); for (String aliasSub : aliasesSub) { this.removeCommand(parentLabel + "." + aliasSub, true); this.unregisterSubCommands(parentLabel + "." + aliasSub, subcommand.getSubcommands()); diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java b/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java index 6a34bdf..07f98d0 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java @@ -16,12 +16,12 @@ public class Arguments { /** * The map of the arguments. */ - private final Map arguments; + protected final Map arguments; /** * The logger of the class. */ - private final Logger logger; + protected final Logger logger; /** * Constructor of the class. @@ -374,4 +374,23 @@ public void add(String key, Class type, Object object) { ArgumentValue argumentValue = new ArgumentValue(type, object); this.arguments.put(key, argumentValue); } + + /** + * Check if an argument exists in the map. + * + * @param key The key of the argument. + * @return true if the argument exists, false otherwise. + */ + public boolean has(String key) { + return this.arguments.containsKey(key); + } + + /** + * Get the logger of the class. + * + * @return The logger of the class. + */ + protected Logger getLogger() { + return this.logger; + } } diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java new file mode 100644 index 0000000..d0ad43b --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java @@ -0,0 +1,36 @@ +package fr.traqueur.commands.api.exceptions; + +/** + * Exception thrown when a command registration fails. + * This is a runtime exception as registration failures are typically unrecoverable. + */ +public class CommandRegistrationException extends RuntimeException { + + /** + * Constructs a new exception with the specified detail message. + * + * @param message the detail message + */ + public CommandRegistrationException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + */ + public CommandRegistrationException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause. + * + * @param cause the cause of the exception + */ + public CommandRegistrationException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java new file mode 100644 index 0000000..6fc453a --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java @@ -0,0 +1,36 @@ +package fr.traqueur.commands.api.exceptions; + +/** + * Exception thrown when the updater fails to initialize. + * This is a runtime exception as initialization failures are typically unrecoverable. + */ +public class UpdaterInitializationException extends RuntimeException { + + /** + * Constructs a new exception with the specified detail message. + * + * @param message the detail message + */ + public UpdaterInitializationException(String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + */ + public UpdaterInitializationException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause. + * + * @param cause the cause of the exception + */ + public UpdaterInitializationException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java index e4c0a43..1aa3440 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java @@ -41,63 +41,209 @@ public CommandInvoker(CommandManager manager) { * @return true if a command handler was executed or a message sent; false if command not found */ public boolean invoke(S source, String base, String[] rawArgs) { - // find matching node + Optional> contextOpt = findCommandContext(base, rawArgs); + if (!contextOpt.isPresent()) { + return false; + } + + CommandContext context = contextOpt.get(); + + if (!validateCommandExecution(source, context)) { + return true; + } + + return executeCommand(source, context); + } + + /** + * Find and prepare command context. + * @param base the base command label + * @param rawArgs the raw arguments + * @return the command context if found + */ + private Optional> findCommandContext(String base, String[] rawArgs) { Optional> found = manager.getCommands().findNode(base, rawArgs); - if (!found.isPresent()) return false; + if (!found.isPresent()) { + return Optional.empty(); + } + MatchResult result = found.get(); CommandTree.CommandNode node = result.node; Optional> cmdOpt = node.getCommand(); - if (!cmdOpt.isPresent()) return false; + + if (!cmdOpt.isPresent()) { + return Optional.empty(); + } + Command command = cmdOpt.get(); String label = node.getFullLabel() != null ? node.getFullLabel() : base; String[] args = result.args; - // in-game check + return Optional.of(new CommandContext<>(command, label, args)); + } + + /** + * Validate command execution conditions (in-game, permissions, requirements, usage). + * @param source the command sender + * @param context the command context + * @return true if all validations passed, false otherwise (message already sent to user) + */ + private boolean validateCommandExecution(S source, CommandContext context) { + return checkInGameOnly(source, context.command) + && checkPermission(source, context.command) + && checkRequirements(source, context.command) + && checkUsage(source, context); + } + + /** + * Check if command requires in-game execution. + * @param source the command sender + * @param command the command to check + * @return true if check passed or not applicable + */ + private boolean checkInGameOnly(S source, Command command) { if (command.inGameOnly() && !manager.getPlatform().isPlayer(source)) { manager.getPlatform().sendMessage(source, manager.getMessageHandler().getOnlyInGameMessage()); - return true; + return false; } - // permission check + return true; + } + + /** + * Check if sender has required permission. + * @param source the command sender + * @param command the command to check + * @return true if check passed or no permission required + */ + private boolean checkPermission(S source, Command command) { String perm = command.getPermission(); if (!perm.isEmpty() && !manager.getPlatform().hasPermission(source, perm)) { manager.getPlatform().sendMessage(source, manager.getMessageHandler().getNoPermissionMessage()); - return true; + return false; } - // requirements + return true; + } + + /** + * Check if all requirements are satisfied. + * @param source the command sender + * @param command the command to check + * @return true if all requirements passed + */ + private boolean checkRequirements(S source, Command command) { for (Requirement req : command.getRequirements()) { if (!req.check(source)) { - String msg = req.errorMessage().isEmpty() - ? manager.getMessageHandler().getRequirementMessage().replace("%requirement%", req.getClass().getSimpleName()) - : req.errorMessage(); + String msg = buildRequirementMessage(req); manager.getPlatform().sendMessage(source, msg); - return true; + return false; } } - // usage check + return true; + } + + /** + * Build error message for failed requirement. + * @param req the failed requirement + * @return the error message + */ + private String buildRequirementMessage(Requirement req) { + return req.errorMessage().isEmpty() + ? manager.getMessageHandler().getRequirementMessage() + .replace("%requirement%", req.getClass().getSimpleName()) + : req.errorMessage(); + } + + /** + * Check if argument count is valid. + * @param source the command sender + * @param context the command context + * @return true if usage is correct + */ + private boolean checkUsage(S source, CommandContext context) { + Command command = context.command; + String[] args = context.args; + int min = command.getArgs().size(); int max = command.isInfiniteArgs() ? Integer.MAX_VALUE : min + command.getOptinalArgs().size(); + if (args.length < min || args.length > max) { - String usage = command.getUsage().isEmpty() - ? command.generateDefaultUsage(manager.getPlatform(), source, label) - : command.getUsage(); + String usage = buildUsageMessage(source, context); manager.getPlatform().sendMessage(source, usage); - return true; + return false; } - // parse and execute + return true; + } + + /** + * Build usage message for command. + * @param source the command sender + * @param context the command context + * @return the usage message + */ + private String buildUsageMessage(S source, CommandContext context) { + Command command = context.command; + String label = context.label; + + return command.getUsage().isEmpty() + ? command.generateDefaultUsage(manager.getPlatform(), source, label) + : command.getUsage(); + } + + /** + * Execute the command with error handling. + * @param source the command sender + * @param context the command context + * @return true if execution succeeded or error was handled, false for internal errors + */ + private boolean executeCommand(S source, CommandContext context) { try { - Arguments parsed = manager.parse(command, args); - command.execute(source, parsed); + Arguments parsed = manager.parse(context.command, context.args); + context.command.execute(source, parsed); + return true; } catch (TypeArgumentNotExistException e) { - manager.getPlatform().sendMessage(source, "&cInternal error: invalid argument type"); - return false; + return handleTypeArgumentError(source); } catch (ArgumentIncorrectException e) { - String msg = manager.getMessageHandler().getArgNotRecognized().replace("%arg%", e.getInput()); - manager.getPlatform().sendMessage(source, msg); - return true; + return handleArgumentIncorrectError(source, e); } + } + + /** + * Handle type argument not exist error. + * @param source the command sender + * @return false to indicate internal error + */ + private boolean handleTypeArgumentError(S source) { + manager.getPlatform().sendMessage(source, "&cInternal error: invalid argument type"); + return false; + } + + /** + * Handle incorrect argument error. + * @param source the command sender + * @param e the exception + * @return true to indicate error was handled + */ + private boolean handleArgumentIncorrectError(S source, ArgumentIncorrectException e) { + String msg = manager.getMessageHandler().getArgNotRecognized().replace("%arg%", e.getInput()); + manager.getPlatform().sendMessage(source, msg); return true; } + /** + * Internal context class to hold command execution data. + */ + private static class CommandContext { + final Command command; + final String label; + final String[] args; + + CommandContext(Command command, String label, String[] args) { + this.command = command; + this.label = label; + this.args = args; + } + } + /** * Suggests command completions based on the provided source, base label, and arguments. * This method checks for available tab completers and filters suggestions based on the current input. diff --git a/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java b/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java index 93c11e5..8da7a73 100644 --- a/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java +++ b/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java @@ -1,5 +1,7 @@ package fr.traqueur.commands.api.updater; +import fr.traqueur.commands.api.exceptions.UpdaterInitializationException; + import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -22,7 +24,7 @@ public class Updater { try { URL_LATEST_RELEASE = URI.create("https://api.github.com/repos/Traqueur-dev/CommandsAPI/releases/latest").toURL(); } catch (MalformedURLException e) { - throw new RuntimeException(e); + throw new UpdaterInitializationException("Failed to initialize updater URL", e); } } diff --git a/gradle.properties b/gradle.properties index 859570d..50d9458 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=4.2.3 \ No newline at end of file +version=4.3.0 \ No newline at end of file diff --git a/jda-test-bot/build.gradle b/jda-test-bot/build.gradle new file mode 100644 index 0000000..5e62de5 --- /dev/null +++ b/jda-test-bot/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'application' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(":jda") + implementation("net.dv8tion:JDA:5.2.1") { + exclude module: 'opus-java' + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +application { + mainClass = 'fr.traqueur.commands.test.TestBot' +} + +tasks.register('runBot', JavaExec) { + group = 'application' + description = 'Run the test Discord bot' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'fr.traqueur.commands.test.TestBot' +} \ No newline at end of file diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java new file mode 100644 index 0000000..71fc4cc --- /dev/null +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java @@ -0,0 +1,74 @@ +package fr.traqueur.commands.test; + +import fr.traqueur.commands.jda.CommandManager; +import fr.traqueur.commands.test.commands.*; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.requests.GatewayIntent; + +import java.util.logging.Logger; + +/** + * Test bot to demonstrate the JDA CommandsAPI. + * + * To run this bot: + * 1. Set the DISCORD_BOT_TOKEN environment variable + * 2. Set the DISCORD_GUILD_ID environment variable (optional, for testing) + * 3. Run with: ./gradlew :jda-test-bot:runBot + */ +public class TestBot { + + private static final Logger LOGGER = Logger.getLogger(TestBot.class.getName()); + + public static void main(String[] args) { + String token = System.getenv("DISCORD_BOT_TOKEN"); + if (token == null || token.isEmpty()) { + LOGGER.severe("DISCORD_BOT_TOKEN environment variable not set!"); + LOGGER.info("Please set your Discord bot token with: export DISCORD_BOT_TOKEN=your_token_here"); + return; + } + + try { + new TestBot(token); + } catch (Exception e) { + LOGGER.severe("Failed to start bot: " + e.getMessage()); + e.printStackTrace(); + } + } + + public TestBot(String token) throws InterruptedException { + LOGGER.info("Starting Discord bot..."); + + // Build JDA instance + JDA jda = JDABuilder.createDefault(token) + .enableIntents(GatewayIntent.GUILD_MEMBERS, GatewayIntent.MESSAGE_CONTENT) + .build() + .awaitReady(); + + LOGGER.info("Bot is ready! Logged in as: " + jda.getSelfUser().getAsTag()); + + // Create command manager + CommandManager commandManager = new CommandManager<>(this, jda, LOGGER); + commandManager.setDebug(true); + + // Register commands + LOGGER.info("Registering commands..."); + commandManager.registerCommand(new PingCommand(this)); + commandManager.registerCommand(new UserInfoCommand(this)); + commandManager.registerCommand(new MathCommand(this)); + commandManager.registerCommand(new GreetCommand(this)); + commandManager.registerCommand(new AdminCommand(this)); + + // Sync commands + String guildId = System.getenv("DISCORD_GUILD_ID"); + if (guildId != null && !guildId.isEmpty()) { + LOGGER.info("Syncing commands to guild " + guildId + " (instant update)..."); + commandManager.syncCommandsToGuild(guildId); + } else { + LOGGER.info("Syncing commands globally (may take up to 1 hour)..."); + commandManager.syncCommands(); + } + + LOGGER.info("Bot is fully operational!"); + } +} \ No newline at end of file diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java new file mode 100644 index 0000000..f892eba --- /dev/null +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java @@ -0,0 +1,181 @@ +package fr.traqueur.commands.test.commands; + +import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.jda.Command; +import fr.traqueur.commands.jda.JDAArguments; +import fr.traqueur.commands.test.TestBot; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +/** + * Admin command with subcommand groups to test JDAPlatform.addCommand() with parts.length >= 3 + * Structure: + * - /admin users kick + * - /admin users ban + * - /admin server info + * - /admin server settings