diff --git a/src/me/despawningbone/discordbot/command/Command.java b/src/me/despawningbone/discordbot/command/Command.java index 6486a53..e2ba553 100644 --- a/src/me/despawningbone/discordbot/command/Command.java +++ b/src/me/despawningbone/discordbot/command/Command.java @@ -1,205 +1,208 @@ package me.despawningbone.discordbot.command; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; //import java.time.OffsetDateTime; //import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import org.apache.commons.lang3.exception.ExceptionUtils; import me.despawningbone.discordbot.DiscordBot; import me.despawningbone.discordbot.command.CommandResult.CommandResultType; import me.despawningbone.discordbot.utils.MiscUtils; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.TextChannel; import net.dv8tion.jda.api.entities.User; public class Command { //no need to be abstract anymore //make it final to ensure thread safety? protected String name; protected String desc; protected String usage; protected List alias; //fixed-size in this case is negligible, as it will not be edited later on anyways protected List remarks = null; protected boolean isDisabled; protected EnumSet perms = EnumSet.noneOf(Permission.class); //so that not configured commands would have a default of no perms needed, instead of null //WILL NOT ALLOW DENY OVERRIDE FOR NOW protected int botUserLevel; //0 = everyone, 1 = BotMod, 2 = BotOwner; negative value = either BotUser or required perm protected List examples; - protected ExecuteImpl impl; + protected ExecuteImpl impl = null; private Command parent = null; private String cat; //not used; remove? //actually gets used now private LinkedHashMap subCmds = new LinkedHashMap<>(); //preserve order public HashMap subCmdAliases = new HashMap(); //TODO temporary solution //DONE add examples; what about subcmds? //for overriding protected Command() {} - //only used by nested commands, since all explicitly declared commands override execute() - public Command(String name, List aliases, Command.ExecuteImpl exeFunc, String usage, List examples, String desc, List remarks, EnumSet defaultPerms, int botUserLevel, Command parent) { + //only used by nested commands, since all explicitly declared commands override execute(); hot creation of subcommands are possible, but permission setting via !desp settings is not supported + public Command(String name, List aliases, Command.ExecuteImpl exeFunc, String usage, List examples, String desc, List remarks, EnumSet defaultPerms, int botUserLevel) { this.name = name; this.alias = aliases; this.impl = exeFunc; this.usage = usage; this.examples = examples; this.desc = desc; this.remarks = remarks; this.perms = defaultPerms; this.botUserLevel = botUserLevel; - this.parent = parent; } public interface ExecuteImpl { //DONE make this a seperate class to store the ever increasing params? if so, protected constructor since it doesnt need to be instantiated elsewhere other than registerSubCommand here //implemented nested commands instead public CommandResult execute(TextChannel channel, User author, Message msg, String[] args); } + private void setParent(Command parent) { //private so only register function can access to avoid accidental use + this.parent = parent; + } + //for anonymous inner classes if needed public void registerSubCommand(Command subCmd) { + subCmd.setParent(this); //make parent only set on register since subcmds with wrong/no parents can result in finnicky bugs subCmds.put(subCmd.getName(), subCmd); if(subCmd.getAliases() != null) subCmd.getAliases().forEach(a -> subCmdAliases.put(a, subCmd)); } public void registerSubCommand(String name, List aliases, Command.ExecuteImpl exe, String usage, List examples, String desc, List remarks) { //subcmd has its own examples? //DONE subcmd permissions and botuserlevel //probably not needed, as its gonna be binded to guild instead of cmds soon enough registerSubCommand(name, aliases, exe, usage, examples, desc, remarks, EnumSet.noneOf(Permission.class), 0); } public void registerSubCommand(String name, List aliases, Command.ExecuteImpl exe, String usage, List examples, String desc, List remarks, EnumSet defaultPerms, int botUserLevel) { - Command subCmd = new Command(name, aliases, exe, usage, examples, desc, remarks, defaultPerms, botUserLevel, this); - subCmds.put(name, subCmd); - if(aliases != null) aliases.forEach(a -> subCmdAliases.put(a, subCmd)); + Command subCmd = new Command(name, aliases, exe, usage, examples, desc, remarks, defaultPerms, botUserLevel); + registerSubCommand(subCmd); } public boolean hasSubCommand() { return !subCmds.isEmpty(); } public Set getSubCommandNames() { return subCmds.keySet(); } public Command getSubCommand(String name) { Command subCmd = subCmds.get(name); return subCmd == null ? subCmdAliases.get(name) : subCmd; } public Command getParent() { return parent; } public enum BotUserLevel { //DONE? implement this DONE test this DEFAULT, BOT_MOD, BOT_OWNER } public String getCategory() { //is null if it is a subcommand return cat; } public String getName() { return name; } public String getDesc() { return desc; } public String getUsage() { return usage; } public List getAliases() { return alias; } public List getRemarks() { return remarks; } public EnumSet getDefaultPerms() { //DONE make this channel dependent? return perms; } public int getRequiredBotUserLevel() { return botUserLevel; } public List getExamples() { return examples; } public boolean isDisabled() { //FIXED think of what to do with disabledGuild; it makes the command class not thread safe; store in DB instead, and be channel based? return isDisabled; } //even when multiple thread references to one specific command, it should be thread safe because it doesnt modify anything in the execute stage; and even the hashmap is somewhat immutable as it will never be changed after finished loading //i dont think i need to even volatile that hashmap //the only thing i might need to do to make it thread safe is to make the hashmap a concurrenthashmap public CommandResult execute(TextChannel channel, User author, Message msg, String[] args) { //if you want a main command to work with sub commands, just super() this and then write the main command stuff OffsetDateTime timesent = OffsetDateTime.now(); try(Connection con = DiscordBot.db.getConnection(); Statement s = con.createStatement()) { //not the best, but i cant share connection through threads String prefix = MiscUtils.getPrefix(s, channel.getGuild().getId()); if(hasSubCommand()) { if(args.length > 0) { Command subCmd = getSubCommand(args[0].toLowerCase()); if(subCmd != null) { int botUserLevel = subCmd.getRequiredBotUserLevel(); Command temp = subCmd; while(temp.getParent() != null && botUserLevel == 0) { temp = temp.getParent(); botUserLevel = temp.getRequiredBotUserLevel(); //if not set get parent's } String perms = subCmd.hasSubCommand() ? null : MiscUtils.getMissingPerms(MiscUtils.getActivePerms(s, channel, subCmd), botUserLevel, channel.getGuild().getMember(author), channel); //yet again pass to handler if(perms == null) { OffsetDateTime timeReceived = OffsetDateTime.now(); long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS)); System.out.println("subcmd parse Time taken: " + ms + "ms"); return subCmd.execute(channel, author, msg, Arrays.copyOfRange(args, 1, args.length)); } else if(perms.equals("DISABLED") || this.isDisabled) { msg.addReaction("❎").queue(); return new CommandResult(CommandResultType.DISABLED); } else { return new CommandResult(CommandResultType.NOPERMS, perms); } } else { - return new CommandResult(CommandResultType.INVALIDARGS, "Unknown sub command! Check `" + prefix + "help " + this.getName() + "` for more info."); + if(impl == null) return new CommandResult(CommandResultType.INVALIDARGS, "Unknown sub command! Check `" + prefix + "help " + this.getName() + "` for more info."); } - } else { - return new CommandResult(CommandResultType.INVALIDARGS, "Please specify a subcommand. You can view them with `" + prefix + "help " + this.name + "`."); + } else { //only run if no execute definition, if not let it override default messages + if(impl == null) return new CommandResult(CommandResultType.INVALIDARGS, "Please specify a subcommand. You can view them with `" + prefix + "help " + this.name + "`."); } - } else { - return impl.execute(channel, author, msg, args); //if no sub commands } + + return impl.execute(channel, author, msg, args); //fallback if no subcommand found; useful for defining custom messages or default command for a set of subcommands } catch (SQLException e) { return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e)); } } public void executeAsync(TextChannel channel, User author, Message msg, String[] args, Consumer success) { OffsetDateTime timesent = OffsetDateTime.now(); CompletableFuture.supplyAsync(() -> execute(channel, author, msg, args)) .exceptionally(ex -> new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex.getCause()))) //getCause should always work as ex is a CompletionException .thenAccept(success).thenAccept(r -> { //DONE thenAccept parses the CommandResult OffsetDateTime timeReceived = OffsetDateTime.now(); long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS)); System.out.println("Time taken: " + ms + "ms"); }); } }