All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.afarion1.command_handler.command.CommandHandler Maven / Gradle / Ivy

There is a newer version: 0.1.2
Show newest version
package com.github.afarion1.command_handler.command;

import com.github.afarion1.command_handler.annotations.Command;
import com.github.afarion1.command_handler.annotations.Config;
import com.github.afarion1.command_handler.command.config.CommandArgumentConfig;
import com.github.afarion1.command_handler.command.config.CommandConfig;
import com.github.afarion1.command_handler.command.config.CommandConfigBuilder;
import com.github.afarion1.command_handler.command.config.CommandListType;
import com.github.afarion1.command_handler.internal_commands.CmdCommandList;
import com.github.afarion1.command_handler.internal_commands.CmdInspectCommand;
import it.unimi.dsi.fastutil.ints.IntList;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.jetbrains.annotations.NotNull;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.List;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Represents CommandHandler entity. Note that running several CommandHandlers in one
 * project is not yet supported (they would share config files and cooldown tables in
 * database).
 */
@SuppressWarnings("WeakerAccess")
public final class CommandHandler {

    private static final Logger log = LoggerFactory.getLogger(CommandHandler.class);

    private final Map, CommandConfig> commandConfigMap = new HashMap<>();
    private final Map, Function>
            commandSupplierMap = new HashMap<>();
    private final Map> commandAliasesMap = new HashMap<>();
    private final List visibleCommandConfigList = new ArrayList<>();

    private final JDA jda;
    private final ExecutorService executor;
    private final String commandsPrefix;
    private final Color helpColor;
    private final Color errorColor;
    private final Color inspectCommandColor;
    private final boolean enableCommandList;
    private final boolean enableInspectCommand;
    private final boolean cleanDbOnStartup;

    private boolean started = false;

    CommandHandler(CommandHandlerBuilder cfg) {
        this.jda = cfg.jda;
        this.executor = cfg.executor;
        this.commandsPrefix = cfg.commandsPrefix;
        this.helpColor = cfg.commandListColor;
        this.errorColor = cfg.errorColor;
        this.inspectCommandColor = cfg.inspectCommandColor;
        this.enableCommandList = cfg.enableCommandList;
        this.enableInspectCommand = cfg.enableInspectCommand;
        this.cleanDbOnStartup = cfg.cleanDbOnStartup;
    }


    /**
     * Initializes and starts listening for messages. 
* Commands should be registered before calling the method. * @see #registerCommand(Class, Function, CommandConfig) * @see Command */ public void start() { if(started){ throw new IllegalStateException("The handler has already started"); } if (isCommandListEnabled()) { log.info("Command list is enabled, registering the command"); registerCommand(CmdCommandList.class); } else { log.info("Command list is disabled"); } if (isInspectCommandEnabled()) { log.info("Inspect command is enabled, registering the command"); registerCommand(CmdInspectCommand.class); } else { log.info("Inspect command is disabled"); } registerAnnotatedCommands(); Database.init(); if (cleanDbOnStartup) Database.deleteOutdatedEntries(this); jda.addEventListener(new MessageListener(this)); started = true; } /** * Registers a command to be handled.
* {@link Command} annotation could be used alternatively, it is usually more convenient.
* Unlike using annotation to register, using the method provides an option to use a different * from the {@link AbstractCommand} super class constructor for the command class.
* Function and config parameters could be null, overloaded versions of the method are present * for convenience. * @param aClass class of the command * @param function a function to provide instances of the command with different from the * {@link AbstractCommand} super class constructor. A reference to * {@link CommandHandler} is given for super() call inside of the constructor. * If the parameter is null, a constructor with single parameter of * {@link CommandHandler} is expected * * @param config configuration of the command. If the parameter is null, the configuration * {@link CommandConfig} object is expected to be annotated with {@link Config} * inside of the command's class * @param subclass of {@link AbstractCommand} * @see Command * @see Config * @see CommandConfigBuilder */ public void registerCommand(@NotNull Class aClass, Function function, CommandConfig config) { if(started) throw new IllegalStateException("The handler has already started. Command registration is only available " + "before the start."); //call other overloaded versions if argument is null if (function == null) { if (config == null) registerCommand(aClass); else registerCommand(aClass, config); return; } else { if (config == null) { registerCommand(aClass, function); return; } } commandConfigMap.put(aClass, config); commandSupplierMap.put(aClass, function); for (String alias : config.getNameAndAliases()) { commandAliasesMap.put(alias, aClass); } if (config.getListType().equals(CommandListType.LISTED)) visibleCommandConfigList.add(config); log.trace("Registered command {}", config.getName()); } /** * Convenience method, equals to {@link #registerCommand(Class, Function, CommandConfig)} with * 3rd parameter of null. * @see #registerCommand(Class, Function, CommandConfig) */ public void registerCommand(Class aClass, Function supplier) { CommandConfig config = getAnnotatedCommandConfig(aClass); registerCommand(aClass, supplier, config); } /** * Convenience method, equals to {@link #registerCommand(Class, Function, CommandConfig)} with * 2nd parameter of null. * @see #registerCommand(Class, Function, CommandConfig) */ public void registerCommand(Class aClass, CommandConfig config) { Function supplier = commandConstructorSupplier(aClass); registerCommand(aClass, supplier, config); } /** * Convenience method, equals to {@link #registerCommand(Class, Function, CommandConfig)} with * 2nd and 3rd parameters of null. * @see #registerCommand(Class, Function, CommandConfig) */ public void registerCommand(Class aClass) { Function supplier = commandConstructorSupplier(aClass); CommandConfig config = getAnnotatedCommandConfig(aClass); registerCommand(aClass, supplier, config); } /** * Useful for custom command list implementation. * @return unmodifiable list of all {@link CommandConfig} with {@link CommandListType#LISTED} */ public List getVisibleCommandConfigList() { return Collections.unmodifiableList(visibleCommandConfigList); } /** * Useful for custom command inspection implementation. * @return unmodifiable set of all command names and aliases * @see CmdInspectCommand */ public Set getAllCommandNamesAndAliases() { return Collections.unmodifiableSet(commandAliasesMap.keySet()); } /** * Useful for custom command inspection implementation. * @return a config or null if the config wasn't found * @see CmdInspectCommand */ public CommandConfig findCommandAndGetConfig(String name) { return commandConfigMap.get(commandAliasesMap.get(name)); } /** * @return JDA that is used to listen to messages */ public JDA getJda() { return jda; } /** * @return default prefix for command call recognition */ public String getCommandsPrefix() { return commandsPrefix; } /** * @return color of command list's embed */ public Color getCommandListColor() { return helpColor; } /** * @return color of command error's embed */ public Color getErrorColor() { return errorColor; } /** * @return color of inspect command's embed */ public Color getInspectCommandColor() { return inspectCommandColor; } /** * @return true if command list is enabled, false otherwise */ public boolean isCommandListEnabled() { return enableCommandList; } /** * @return true if inspect command is enabled, false otherwise */ public boolean isInspectCommandEnabled() { return enableInspectCommand; } /** * @return a config or null if the config wasn't found */ CommandConfig getCommandConfig(Class aClass) { return commandConfigMap.get(aClass); } /** * @return unmodifiable collection of all command's configs */ Collection getCommandConfigList() { return Collections.unmodifiableCollection(commandConfigMap.values()); } /** * @return executor that is used to process commands */ ExecutorService getExecutor() { return executor; } private void registerAnnotatedCommands() { Reflections refl = new Reflections(""); Set> annotated = refl.getTypesAnnotatedWith(Command.class); @SuppressWarnings("unchecked") List> commands = annotated.stream() .filter(t -> t.getSuperclass() == AbstractCommand.class) .map(t -> (Class) t) .collect(Collectors.toList()); for (Class cmd : commands) { registerCommand(cmd); } } /** * @return command's class or null if wasn't found */ Class findCommand(String name) { return commandAliasesMap.get(name); } /** * @return new instance if command is registered or null otherwise */ T newCommandInstance(Class aClass) { if (aClass == null) return null; @SuppressWarnings("unchecked") T instance = (T) commandSupplierMap.get(aClass).apply(this); return instance; } @NotNull private CommandConfig getAnnotatedCommandConfig(Class aClass) { Method configMethod = null; for (Method declaredMethod : aClass.getDeclaredMethods()) { if (declaredMethod.isAnnotationPresent(Config.class)) { configMethod = declaredMethod; break; } } CommandConfig config; if (configMethod != null) { Object methodResult; try { configMethod.setAccessible(true); methodResult = configMethod.invoke(this, (Object[]) null); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException("Unable to get CommandConfig", e); } if (methodResult instanceof CommandConfig) config = (CommandConfig) methodResult; else throw new IllegalArgumentException("Method annotated with @Config is expected to return CommandConfig, but returned " + methodResult); } else { Field configField = null; for (Field declaredField : aClass.getDeclaredFields()) { if (declaredField.isAnnotationPresent(Config.class)) { configField = declaredField; break; } } if (configField == null) throw new RuntimeException("Couldn't find any config method/field. Please create a method/field that" + "will contain/return CommandConfig."); Object fieldContent; try { configField.setAccessible(true); fieldContent = configField.get(this); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to get CommandConfig", e); } if (fieldContent instanceof CommandConfig) config = (CommandConfig) fieldContent; else throw new IllegalArgumentException("Field annotated with @Config is expected to contain CommandConfig, but contains " + fieldContent); } return config; } @NotNull private Function commandConstructorSupplier(Class aClass) { return ch -> { try { return aClass.getConstructor(CommandHandler.class).newInstance(ch); } catch (Exception e) { throw new RuntimeException("Unable to create an instance of command. (If command has no constructor " + "without parameters, then a supplier should be provided using another overloaded version of " + "register method)", e); } }; } void processCommand(MessageReceivedEvent event) { //remove prefix String commandString = event.getMessage().getContentRaw().substring(commandsPrefix.length()).trim(); if (commandString.length() < 1) { log.trace("Nothing after prefix"); return; } //find longest cmd name or alias match log.trace("Finding cmd name in the message"); FindCommand findCommand = new FindCommand(commandString).invoke(); AbstractCommand command = findCommand.getCommand(); String cmdNameFoundBy = findCommand.getCmdFoundBy(); if (command == null) return; log.trace("Found command {} by name {}", command.getName(), cmdNameFoundBy); if (command.isExecuteInGuildOnly(event) && event.getGuild() == null) { log.trace("The command could be executed only in server chat, aborting"); event.getChannel().sendMessage("This command could be executed only in server chat.").queue(); return; } boolean shouldExecuteIfCantCheckOrSaveCooldown = command.shouldExecuteIfCantCheckOrSaveCooldown(event); if (Database.noConnection() && command.hasAnyCooldown(event) && !shouldExecuteIfCantCheckOrSaveCooldown) { log.debug("Unable to execute the command due to DB issues."); event.getChannel().sendMessage("Unable to execute the command due to DB issues").queue(); return; } //separate args String argumentString = commandString.substring(cmdNameFoundBy.length()); //process args log.trace("Processing arguments"); CommandArguments cmdArgs; if (command.isRawArgs()) cmdArgs = CommandArgumentsFactory.processRawArguments(argumentString); else cmdArgs = CommandArgumentsFactory.processArguments(command, argumentString, event); if (!cmdArgs.areValid()) { log.trace("Arguments are invalid"); sendErrorMessage(event, command, cmdArgs); return; } log.trace("Arguments are valid"); List unsatisfiedPermissions = command.getUnsatisfiedPermissions(event); if (!unsatisfiedPermissions.isEmpty()) { sendNotEnoughPermsMessage(event, unsatisfiedPermissions); return; } log.trace("Enough discord permissions"); try{ if(command.hasUserCooldown(event)){ long cooledDownAfter = Database.getUserCooledDownDate(event.getAuthor().getIdLong(), command.getName()); if(System.currentTimeMillis() < cooledDownAfter){ event.getChannel().sendMessage("The command is on cooldown: " + DurationFormatUtils.formatDurationWords( cooledDownAfter - System.currentTimeMillis(), true, true)).queue(); log.debug("{} is on per user cooldown.", command.getName()); return; } } } catch (SQLException e) { handleCooldownSQLException(event, e); if(!shouldExecuteIfCantCheckOrSaveCooldown) return; } try{ if(command.hasGuildCooldown(event)){ long cooledDownAfter = Database.getGuildCooledDownDate(event.getGuild().getIdLong(), command.getName()); if(System.currentTimeMillis() < cooledDownAfter){ event.getChannel().sendMessage("The command is on cooldown: " + DurationFormatUtils.formatDurationWords( cooledDownAfter - System.currentTimeMillis(), true, true)).queue(); log.debug("{} is on per user cooldown.", command.getName()); return; } } } catch (SQLException e) { handleCooldownSQLException(event, e); if(!shouldExecuteIfCantCheckOrSaveCooldown) return; } try { if (command.hasUserCooldown(event)) { long cooledDownAfter = System.currentTimeMillis() + command.getUserCooldown(event).toMillis(); Database.saveCommandUserCooldown(event.getAuthor().getIdLong(), command.getName(), cooledDownAfter); } if(command.hasGuildCooldown(event)) { long cooledDownAfter = System.currentTimeMillis() + command.getGuildCooldown(event).toMillis(); Database.saveCommandGuildCooldown(event.getGuild().getIdLong(), command.getName(), cooledDownAfter); } } catch (SQLException e) { handleCooldownSQLException(event, e); if(!shouldExecuteIfCantCheckOrSaveCooldown) return; } executeCommand(event, command, cmdArgs); } private void handleCooldownSQLException(MessageReceivedEvent event, SQLException e) { log.error("Error while managing command's cooldown.", e); event.getChannel().sendMessage("Something went wrong while trying to manage cooldown of the command." + " Aborting execution.").queue(); } private void sendNotEnoughPermsMessage(MessageReceivedEvent event, List unsatisfiedPermissions) { StringBuilder sb = new StringBuilder(); sb.append("You don't have "); sb.append(unsatisfiedPermissions.get(0).getName()); for (int i = 1; i < unsatisfiedPermissions.size(); i++) { sb.append(", "); sb.append(unsatisfiedPermissions.get(i).getName()); } sb.append(" permission"); if (unsatisfiedPermissions.size() > 1) sb.append("s"); sb.append("in order to execute this command."); event.getChannel().sendMessage(sb.toString()).queue(); log.debug("Not enough discord permissions: {}", unsatisfiedPermissions); } private void sendErrorMessage(MessageReceivedEvent event, AbstractCommand command, CommandArguments cmdArgs) { EmbedBuilder eb = new EmbedBuilder(); eb.setTitle("Wrong command usage"); eb.setColor(getErrorColor()); eb.setDescription(command.getCmdWithArgsSignature() + "\n\nWrong arguments:"); appendWrongArgs(command, cmdArgs, eb); event.getChannel().sendMessage(eb.build()).queue(); } private void executeCommand(MessageReceivedEvent event, AbstractCommand command, CommandArguments cmdArgs) { try { log.debug("Executing command {} sent by {} in channel id {}", command.getName(), event.getAuthor().getAsMention(), event.getChannel().getId()); command.execute(event, cmdArgs); log.debug("Finished execution of command {} sent by {} in channel id {}", command.getName(), event.getAuthor().getAsMention(), event.getChannel().getId()); } catch (Exception e) { log.error("Error while executing command", e); event.getChannel().sendMessage("Something went wrong while executing the command").queue(); } } private static void appendWrongArgs(AbstractCommand cmd, CommandArguments args, EmbedBuilder eb) { IntList wrongArgsIds = args.getWrongArgsIds(); for (CommandArgumentConfig arg : cmd.getCommandArguments()) { if (wrongArgsIds.contains(arg.getId())) { String fieldName = arg.getArgumentName().toLowerCase(); String errorMsg = arg.getErrorMsg().length() > 0 ? arg.getErrorMsg() : arg.getArgumentDesc(); eb.addField(fieldName, errorMsg, true); } } } private class FindCommand { private final String msg; private AbstractCommand command = null; private String cmdFoundBy = null; private FindCommand(String msg) { this.msg = msg; } private AbstractCommand getCommand() { return command; } private String getCmdFoundBy() { return cmdFoundBy; } private FindCommand invoke() { //choose longest option to avoid excessive arguments bug log.trace("All commands names and aliases: {}", getAllCommandNamesAndAliases()); Optional commandName = getAllCommandNamesAndAliases().stream() .map(String::toLowerCase) .filter(name -> msg.toLowerCase().indexOf(name) == 0) .max(Comparator.comparingInt(String::length)); if (commandName.isPresent()) { cmdFoundBy = commandName.get(); Class aClass = findCommand(cmdFoundBy); command = newCommandInstance(aClass); } return this; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy