pingex
/
DiscordBot
Archived
1
0
Fork 0

Discord chat commands parser + dispatcher

master
Pingex aka Raphaël 9 years ago
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();
}

@ -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()

@ -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);
}
}

@ -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<AbstractModule> getRegistry()
{
return (ArrayList<AbstractModule>) datastore.clone();
}
}