Discord chat commands parser + dispatcher
parent
3d5ec1c537
commit
fd219962fc
@ -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;
|
||||||
|
}
|
@ -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<String, InvokableMethod> 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<AbstractModule> 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");
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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<String, String[]> getShorthands();
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue