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