diff --git a/src/main/java/net/pingex/discordbot/Command.java b/src/main/java/net/pingex/discordbot/Command.java new file mode 100644 index 0000000..671f3d2 --- /dev/null +++ b/src/main/java/net/pingex/discordbot/Command.java @@ -0,0 +1,21 @@ +package net.pingex.discordbot; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a Command as callable from the Discord chat. + * @version 0.1-dev + * @author Raphael "Pingex" NAAS + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Command +{ + /** + * Minimum number of arguments expected + */ + int minArgs() default 0; +} diff --git a/src/main/java/net/pingex/discordbot/CommandDispatcher.java b/src/main/java/net/pingex/discordbot/CommandDispatcher.java new file mode 100644 index 0000000..5965eb4 --- /dev/null +++ b/src/main/java/net/pingex/discordbot/CommandDispatcher.java @@ -0,0 +1,105 @@ +package net.pingex.discordbot; + +import sx.blah.discord.api.EventSubscriber; +import sx.blah.discord.handle.impl.events.MessageReceivedEvent; +import sx.blah.discord.util.DiscordException; +import sx.blah.discord.util.HTTP429Exception; +import sx.blah.discord.util.MissingPermissionsException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class parses and dispatch Discord commands + * @version 0.1-dev + * @author Raphael "Pingex" NAAS + */ +public class CommandDispatcher +{ + private Logger logger; + + /** + * Contains all available commands, built using `rebuildCommandList()` + */ + private HashMap commandList; + + /** + * Basic Constructor, automatically rebuilds command list from the modules registry + */ + public CommandDispatcher() + { + logger = Logger.getLogger(this.getClass().getName()); + rebuildCommandList(); + } + + /** + * Fired when receiving a message + * @param event Event from Discord API + */ + @EventSubscriber + public void onMessageReceivedEvent(MessageReceivedEvent event) + { + Matcher m = Pattern.compile("^!(\\w+):(\\w+)(?: (.*))?$").matcher(event.getMessage().getContent()); // TODO: Custom command prefix + if(!m.matches()) return; // We don't need to go further if it's not even a command + + String module = m.group(1); + String command = m.group(2); + String fullCommand = module + ":" + command; + String[] args = (m.group(3) != null) ? m.group(3).split(" ") : null; + + logger.info("Command invoked (" + event.getMessage().getAuthor().getName() + "): " + fullCommand); + + try + { + if(commandList.containsKey(fullCommand)) + { + String answer = (String) commandList.get(fullCommand).invoke(new Object[]{args}); // Bricolage + if(answer != null) event.getMessage().reply(answer); + } + else + event.getMessage().reply("Unknown command"); + } catch (MissingPermissionsException | HTTP429Exception | DiscordException e) + { + logger.warning("Couldn't reply to command: " + e.getMessage()); + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) + { + logger.severe("Couldn't call target method: " + e.getMessage()); + } + } + + /** + * Rebuilds command list using the module registry. + * - It looks for methods with @Command annotation inside class with @Controllable annotation + * - Check his prototype (should be `method(String[]):String`) + * - Add usable methods in his local registry, using InvokableMethod + * @see InvokableMethod + * @see ModulesRegistry + */ + public void rebuildCommandList() + { + logger.info("Rebuilding command list..."); + + ArrayList registry = ModulesRegistry.getRegistry(); + commandList = new HashMap<>(); + + for(AbstractModule i : registry) + if(i.getClass().isAnnotationPresent(Controllable.class)) + for(Method j : i.getClass().getDeclaredMethods()) + if(j.isAnnotationPresent(Command.class)) + { + String id = i.getClass().getAnnotation(Controllable.class).name() + ":" + j.getName(); + logger.info("Found " + id); + + if(j.getParameterCount() == 1 && j.getParameterTypes()[0] == String[].class && j.getReturnType() == String.class) + commandList.put(id, new InvokableMethod(j, i)); + else + logger.warning(id + ": incorrect function prototype, thus won't be added to the command list."); + } + + logger.info("... Done"); + } +} diff --git a/src/main/java/net/pingex/discordbot/Controllable.java b/src/main/java/net/pingex/discordbot/Controllable.java new file mode 100644 index 0000000..44661b2 --- /dev/null +++ b/src/main/java/net/pingex/discordbot/Controllable.java @@ -0,0 +1,21 @@ +package net.pingex.discordbot; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a module as Controllable, ie which contains commands callable from the Discord chat. + * @version 0.1-dev + * @author Raphael "Pingex" NAAS + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Controllable +{ + /** + * Name of the module, ie the `module` part in `!module:command` + */ + String name(); +} diff --git a/src/main/java/net/pingex/discordbot/DiscordBot.java b/src/main/java/net/pingex/discordbot/DiscordBot.java index 9e8b2fc..ad120e6 100644 --- a/src/main/java/net/pingex/discordbot/DiscordBot.java +++ b/src/main/java/net/pingex/discordbot/DiscordBot.java @@ -36,6 +36,7 @@ public class DiscordBot LOGGER.info("Logged in as \"" + event.getClient().getOurUser().getName() + "#" + event.getClient().getOurUser().getDiscriminator() + "\" (#" + event.getClient().getOurUser().getID() + ")")); enableModules(); + this.client.getDispatcher().registerListener(new CommandDispatcher()); } private void enableModules() diff --git a/src/main/java/net/pingex/discordbot/IControllable.java b/src/main/java/net/pingex/discordbot/IControllable.java deleted file mode 100644 index 6dc839e..0000000 --- a/src/main/java/net/pingex/discordbot/IControllable.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.pingex.discordbot; - -import java.util.Hashtable; - -/** - * Implemented by modules which can be controlled using Discord Commands - * @version 0.1-dev - * @author Raphael "Pingex" NAAS - */ -public interface IControllable -{ - /** - * A list of prefixes the module uses to identify itself. - * @return List of prefixes - */ - String[] getPrefixes(); - - /** - * Get a list of shortened commands. - * Example: command !module:myCommand can be shortened as !myc, !mc, or !myco, so the table would be like "myCommand" => ["myc", "mc", "myco"] - * @return Hastable containing all shortened commands. Key is the real command, Value is a list of eligible short commands. - */ - Hashtable getShorthands(); -} diff --git a/src/main/java/net/pingex/discordbot/InvokableMethod.java b/src/main/java/net/pingex/discordbot/InvokableMethod.java new file mode 100644 index 0000000..4dd697f --- /dev/null +++ b/src/main/java/net/pingex/discordbot/InvokableMethod.java @@ -0,0 +1,26 @@ +package net.pingex.discordbot; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Helper class which defines a pair of Method/Object used for direct invoking. + * @version 0.1-dev + * @author Raphael "Pingex" NAAS + */ +public class InvokableMethod +{ + public Method method; + public Object object; + + public InvokableMethod(Method m, Object o) + { + method = m; + object = o; + } + + public Object invoke(Object... args) throws InvocationTargetException, IllegalAccessException, IllegalArgumentException + { + return method.invoke(object, args); + } +} diff --git a/src/main/java/net/pingex/discordbot/ModulesRegistry.java b/src/main/java/net/pingex/discordbot/ModulesRegistry.java index 66d67b8..3badccd 100644 --- a/src/main/java/net/pingex/discordbot/ModulesRegistry.java +++ b/src/main/java/net/pingex/discordbot/ModulesRegistry.java @@ -20,4 +20,14 @@ public class ModulesRegistry { datastore.add(toRegister); } + + /** + * Returns of the whole registry + * @return A clone of the whole registry, to avoid damaging the original registry + */ + @SuppressWarnings("unchecked") + public static ArrayList getRegistry() + { + return (ArrayList) datastore.clone(); + } }