From eb8ec724061d1ab23d6362ad6375dc3abef172d1 Mon Sep 17 00:00:00 2001 From: Pingex Date: Thu, 1 Sep 2016 20:24:24 +0200 Subject: [PATCH] Internal commands and command parser. --- .../dcf/DiscordCommandableFramework.java | 5 + .../pingex/dcf/commands/InternalCommands.java | 152 ++++++++++++++++++ .../net/pingex/dcf/util/ArgumentParser.java | 105 ++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 src/main/java/net/pingex/dcf/commands/InternalCommands.java create mode 100644 src/main/java/net/pingex/dcf/util/ArgumentParser.java diff --git a/src/main/java/net/pingex/dcf/DiscordCommandableFramework.java b/src/main/java/net/pingex/dcf/DiscordCommandableFramework.java index 47f18cf..e345038 100644 --- a/src/main/java/net/pingex/dcf/DiscordCommandableFramework.java +++ b/src/main/java/net/pingex/dcf/DiscordCommandableFramework.java @@ -1,5 +1,7 @@ package net.pingex.dcf; +import net.pingex.dcf.commands.CommandRegistry; +import net.pingex.dcf.commands.InternalCommands; import net.pingex.dcf.core.Configuration; import net.pingex.dcf.core.GatewayConnectionsManager; import net.pingex.dcf.modularity.PluginLoader; @@ -40,6 +42,9 @@ public class DiscordCommandableFramework else builder.withLogin(Configuration.CONNECTION_USERNAME, Configuration.CONNECTION_PASSWORD); GatewayConnectionsManager.registerConnection(builder); + // Register internal commands + new InternalCommands().getCommands().forEach(CommandRegistry::registerCommand); + // Run plugins PluginLoader.getInstance().bulkRunPlugins(); } diff --git a/src/main/java/net/pingex/dcf/commands/InternalCommands.java b/src/main/java/net/pingex/dcf/commands/InternalCommands.java new file mode 100644 index 0000000..d585ae1 --- /dev/null +++ b/src/main/java/net/pingex/dcf/commands/InternalCommands.java @@ -0,0 +1,152 @@ +package net.pingex.dcf.commands; + +import net.pingex.dcf.core.Configuration; +import net.pingex.dcf.util.ArgumentParser; +import net.pingex.dcf.util.DiscordInteractionsUtil; +import org.apache.commons.lang3.StringUtils; +import sx.blah.discord.handle.impl.events.MessageReceivedEvent; +import java.util.*; + +/** + * Internal commands of DCF. + */ +public class InternalCommands implements IWithCommands +{ + @Override + public Set getCommands() + { + return new HashSet<>(Arrays.asList(ListCommand.INSTANCE, UsageCommand.INSTANCE)); + } + + /** + * This command list all available commands on DCF. + */ + private static class ListCommand extends Command + { + private static final String NAME = "internal:list"; + private static final List ALIASES = Arrays.asList("list", "help"); + private static final String DESCRIPTION = "List all available commands."; + private static final boolean IS_ENABLED = true; + private static final String USAGE = Configuration.COMMAND_PREFIX + NAME + " "; + + /** + * How many commands should be displayed on each page + */ + public static final int COMMANDS_PER_PAGE = 5; + + static final ListCommand INSTANCE = new ListCommand(); + + private ListCommand() + { + super(NAME, ALIASES, DESCRIPTION, IS_ENABLED, USAGE); + } + + @Override + public void execute(MessageReceivedEvent event, List arguments) + { + // Parameters + Set bank = CommandRegistry.getRegistry(); + int amountPages = (int) Math.ceil(bank.size()/(double)COMMANDS_PER_PAGE); + int requestedPage; + int longestCommand = bank.stream().max((o1, o2) -> Integer.compare(o1.getName().length(), o2.getName().length())).get().getName().length(); + int longestDesc = bank.stream().max((o1, o2) -> Integer.compare(o1.getDescription().length(), o2.getDescription().length())).get().getDescription().length(); + + // Parsing + try + { + List output = ArgumentParser.parseAll(Collections.singletonList(Integer.class), arguments); + requestedPage = output.get(0) != null ? (int) output.get(0) : 1; + } + catch(ArgumentParser.ParserException e) + { + DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), e.getMessage()); + return; + } + + // Checks + if(requestedPage <= 0 || requestedPage > amountPages) + { + DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Requested page is invalid. Number of available pages: " + amountPages); + return; + } + + StringBuilder output = new StringBuilder + ("**List of commands available for " + Configuration.BOT_NAME + "** (page " + requestedPage + "/" + amountPages + ")\n"); + output.append("Use `").append(UsageCommand.USAGE).append("`") + .append(" to get details and usage about any command.\n"); + output.append("```"); + + int pos = 1; + for(Command i : bank) + { + if(pos > (requestedPage-1)*COMMANDS_PER_PAGE && pos <= requestedPage*COMMANDS_PER_PAGE) + { + output.append("+ ").append(StringUtils.rightPad(i.getName(), longestCommand)); // Name + output.append(" ").append(StringUtils.rightPad(i.getDescription(), longestDesc)); // Desc + if(i.getAliases().size() > 0) + output.append(" ").append("(aliases: ").append(StringUtils.join(i.getAliases(), ", ")).append(")"); // Aliases + + output.append("\n"); + } + pos++; + } + + output.append("```"); + DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), output.toString()); + } + } + + /** + * Gives the usage of a command. + */ + private static class UsageCommand extends Command + { + private static final String NAME = "internal:usage"; + private static final List ALIASES = Collections.singletonList("usage"); + private static final String DESCRIPTION = "Gives the usage of a command."; + private static final boolean IS_ENABLED = true; + private static final String USAGE = Configuration.COMMAND_PREFIX + NAME + " "; + + static final UsageCommand INSTANCE = new UsageCommand(); + + private UsageCommand() + { + super(NAME, ALIASES, DESCRIPTION, IS_ENABLED, USAGE); + } + + @Override + public void execute(MessageReceivedEvent event, List arguments) throws Throwable + { + // Checks + if(arguments.size() != 1) // Arg check + { + DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Invalid argument."); + return; + } + + Optional uncheckedTarget = CommandRegistry.getCommandOrAliasByName(arguments.get(0)); + if(!uncheckedTarget.isPresent()) // Command existence + { + DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), "Target command not found."); + return; + } + + // Print everything + Command target = uncheckedTarget.get(); + StringBuilder output = new StringBuilder(); + output.append("**Details and usage for command `") + .append(Configuration.COMMAND_PREFIX).append(target.getName()) + .append("`**\n") + .append("```"); + + output.append("Description: ").append(target.getDescription()).append("\n"); + if(target.getAliases().size() > 0) + output.append("Aliases: ").append(StringUtils.join(target.getAliases(), ", ")).append("\n"); + output.append("Usage: ").append(target.getUsage()).append("\n"); + output.append("Enabled: ").append(target.isEnabled() ? "Yes" : "No").append("\n"); + + output.append("```"); + DiscordInteractionsUtil.sendMessage(event.getMessage().getChannel(), output.toString()); + } + } +} diff --git a/src/main/java/net/pingex/dcf/util/ArgumentParser.java b/src/main/java/net/pingex/dcf/util/ArgumentParser.java new file mode 100644 index 0000000..41b9f22 --- /dev/null +++ b/src/main/java/net/pingex/dcf/util/ArgumentParser.java @@ -0,0 +1,105 @@ +package net.pingex.dcf.util; + +import org.apache.commons.lang3.ClassUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Util class used to parse a String into primitive types. + */ +public class ArgumentParser +{ + /** + * Parses a String into a primitive variable. + * Choice between boolean, byte, short, int, long, float, double, String and their respective wrapper types. + * @param target Target primitive + * @param value Input value + * @return `null` if the value is null or the string content is null, or the parsed value. + * @throws IllegalArgumentException If the target primitive/class isn't part of the previous list. + * @throws NumberFormatException if the cast failed. + */ + public static Object parse(Class target, String value) throws IllegalArgumentException + { + if(value == null || value.equals("null") ) return null; + if(Boolean.class == target || Boolean.TYPE == target) return Boolean.parseBoolean(value); + if(Byte.class == target || Byte.TYPE == target) return Byte.parseByte(value); + if(Short.class == target || Short.TYPE == target) return Short.parseShort(value); + if(Integer.class == target || Integer.TYPE == target) return Integer.parseInt(value); + if(Long.class == target || Long.TYPE == target) return Long.parseLong(value); + if(Float.class == target || Float.TYPE == target) return Float.parseFloat(value); + if(Double.class == target || Double.TYPE == target) return Double.parseDouble(value); + if(String.class == target) return value; + throw new IllegalArgumentException("Unknown target type"); + } + + /** + * Parse the input arguments to a list of argument of various types. + * Use a primitive type for an argument that is required. + * Use a wrapper type to an argument that is optional, thereby the said argument can be {@code null}. + * Any extra argument not specified in {@code targetTypes} are ignored and will not appear in the output list. + * + * @param targetTypes A list of types that the {@code arguments} will be checked against. + * @param arguments Input arguments to check + * @return The parsed arguments whose types matches {@code targetTypes} + * @throws ParserException if the parsing failed. + */ + public static List parseAll(List> targetTypes, List arguments) throws ParserException + { + if(targetTypes.size() == 0) throw new ParserException("Nothing to parse."); + + List outputList = new ArrayList<>(); + + for(int i = 0; i < targetTypes.size(); i++) + { + try + { + Object parsingOutput = parse(targetTypes.get(i), arguments.get(i)); + if(parsingOutput == null) + { + if(ClassUtils.isPrimitiveWrapper(targetTypes.get(i)) || targetTypes.get(i) == String.class) outputList.add(null); // Allows null + else throw new ParserException("Invalid arguments."); // Disallows null + } + else outputList.add(parsingOutput); + } + catch(IndexOutOfBoundsException e) // Not enough arguments supplied + { + if(ClassUtils.isPrimitiveWrapper(targetTypes.get(i)) || targetTypes.get(i) == String.class) outputList.add(null); // Allows null + else throw new ParserException("Not enough arguments supplied."); // Disallows null + } + catch(IllegalArgumentException e) + { + if(e instanceof NumberFormatException) throw new ParserException("Invalid arguments."); // Cannot be casted because the user fucked up + else throw new ParserException("Parsing error, report to dev.", e); // Cannot be casted because the dev fucked up + } + } + + return outputList; + } + + /** + * Thrown whenever any parse operation failed. + */ + public static class ParserException extends RuntimeException + { + public ParserException(String message, Throwable cause) + { + super(message, cause); + } + + public ParserException(Throwable cause) + { + super(cause); + } + + public ParserException() + { + super(); + } + + public ParserException(String message) + { + super(message); + } + } +}