Complete message command parser and listener
All checks were successful
continuous-integration/drone/push Build is passing

The message command listener is now completed and the bot now also supports message-based commands with multiple aliases.
This commit is contained in:
Bea 2022-11-22 16:19:08 +01:00
parent 501b1bc71c
commit a9790b3525
10 changed files with 244 additions and 81 deletions

View File

@ -3,6 +3,7 @@ package wtf.beatrice.hidekobot;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import wtf.beatrice.hidekobot.datasource.ConfigurationSource; import wtf.beatrice.hidekobot.datasource.ConfigurationSource;
import wtf.beatrice.hidekobot.datasource.DatabaseSource; import wtf.beatrice.hidekobot.datasource.DatabaseSource;
import wtf.beatrice.hidekobot.listeners.MessageCommandListener;
import wtf.beatrice.hidekobot.listeners.MessageLogger; import wtf.beatrice.hidekobot.listeners.MessageLogger;
import wtf.beatrice.hidekobot.listeners.SlashCommandListener; import wtf.beatrice.hidekobot.listeners.SlashCommandListener;
import wtf.beatrice.hidekobot.util.Logger; import wtf.beatrice.hidekobot.util.Logger;
@ -26,7 +27,7 @@ public class Cache
// note: discord sets interactions' expiry time to 15 minutes by default, so we can't go higher than that. // note: discord sets interactions' expiry time to 15 minutes by default, so we can't go higher than that.
private final static long expiryTimeSeconds = 15L; private final static long expiryTimeSeconds = 15L;
// used to count eg. uptime // used to count e.g. uptime
private static LocalDateTime startupTime; private static LocalDateTime startupTime;
private final static String execPath = System.getProperty("user.dir"); private final static String execPath = System.getProperty("user.dir");
@ -35,6 +36,7 @@ public class Cache
private static final String botName = "HidekoBot"; private static final String botName = "HidekoBot";
private static SlashCommandListener slashCommandListener = null; private static SlashCommandListener slashCommandListener = null;
private static MessageCommandListener messageCommandListener = null;
private final static String defaultInviteLink = private final static String defaultInviteLink =
"https://discord.com/api/oauth2/authorize?client_id=%userid%&scope=bot+applications.commands&permissions=8"; "https://discord.com/api/oauth2/authorize?client_id=%userid%&scope=bot+applications.commands&permissions=8";
@ -201,14 +203,18 @@ public class Cache
} }
//todo javadocs
public static void setSlashCommandListener(SlashCommandListener commandListener) public static void setSlashCommandListener(SlashCommandListener commandListener)
{ { slashCommandListener = commandListener; }
slashCommandListener = commandListener;
}
public static SlashCommandListener getSlashCommandListener() { return slashCommandListener; } public static SlashCommandListener getSlashCommandListener() { return slashCommandListener; }
public static void setMessageCommandListener(MessageCommandListener commandListener)
{ messageCommandListener = commandListener; }
public static MessageCommandListener getMessageCommandListener() { return messageCommandListener; }
/** /**
* Set the bot's startup time. Generally only used at boot time. * Set the bot's startup time. Generally only used at boot time.
* *

View File

@ -6,11 +6,13 @@ import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.entities.Activity;
import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.requests.GatewayIntent;
import sun.misc.Signal; import sun.misc.Signal;
import wtf.beatrice.hidekobot.commands.message.CommandsCommand;
import wtf.beatrice.hidekobot.commands.message.HelloCommand;
import wtf.beatrice.hidekobot.commands.slash.*; import wtf.beatrice.hidekobot.commands.slash.*;
import wtf.beatrice.hidekobot.datasource.ConfigurationSource; import wtf.beatrice.hidekobot.datasource.ConfigurationSource;
import wtf.beatrice.hidekobot.datasource.DatabaseSource; import wtf.beatrice.hidekobot.datasource.DatabaseSource;
import wtf.beatrice.hidekobot.listeners.ButtonInteractionListener; import wtf.beatrice.hidekobot.listeners.ButtonInteractionListener;
import wtf.beatrice.hidekobot.listeners.MessageListener; import wtf.beatrice.hidekobot.listeners.MessageCommandListener;
import wtf.beatrice.hidekobot.listeners.SlashCommandCompleter; import wtf.beatrice.hidekobot.listeners.SlashCommandCompleter;
import wtf.beatrice.hidekobot.listeners.SlashCommandListener; import wtf.beatrice.hidekobot.listeners.SlashCommandListener;
import wtf.beatrice.hidekobot.runnables.ExpiredMessageTask; import wtf.beatrice.hidekobot.runnables.ExpiredMessageTask;
@ -97,7 +99,7 @@ public class HidekoBot
} }
// register commands // register slash commands
SlashCommandListener slashCommandListener = new SlashCommandListener(); SlashCommandListener slashCommandListener = new SlashCommandListener();
slashCommandListener.registerCommand(new AvatarCommand()); slashCommandListener.registerCommand(new AvatarCommand());
slashCommandListener.registerCommand(new BotInfoCommand()); slashCommandListener.registerCommand(new BotInfoCommand());
@ -110,8 +112,14 @@ public class HidekoBot
slashCommandListener.registerCommand(new SayCommand()); slashCommandListener.registerCommand(new SayCommand());
Cache.setSlashCommandListener(slashCommandListener); Cache.setSlashCommandListener(slashCommandListener);
// register message commands
MessageCommandListener messageCommandListener = new MessageCommandListener();
messageCommandListener.registerCommand(new HelloCommand());
messageCommandListener.registerCommand(new CommandsCommand());
Cache.setMessageCommandListener(messageCommandListener);
// register listeners // register listeners
jda.addEventListener(new MessageListener()); jda.addEventListener(messageCommandListener);
jda.addEventListener(slashCommandListener); jda.addEventListener(slashCommandListener);
jda.addEventListener(new SlashCommandCompleter()); jda.addEventListener(new SlashCommandCompleter());
jda.addEventListener(new ButtonInteractionListener()); jda.addEventListener(new ButtonInteractionListener());

View File

@ -0,0 +1,43 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.objects.MessageCommand;
import java.util.Collections;
import java.util.LinkedList;
public class CommandsCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Collections.singletonList("commands"));
}
@Override
public boolean passRawArgs() {
return false;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args) {
StringBuilder commandsList = new StringBuilder();
commandsList.append("Recognized message commands: ");
LinkedList<MessageCommand> messageCommands = Cache.getMessageCommandListener().getRegisteredCommands();
for(int i = 0; i < messageCommands.size(); i++)
{
commandsList.append("`")
.append(messageCommands.get(i).getCommandLabels().get(0))
.append("`");
if(i+1 != messageCommands.size())
{ commandsList.append(", "); }
}
event.getMessage().reply(commandsList.toString()).queue();
}
}

View File

@ -0,0 +1,29 @@
package wtf.beatrice.hidekobot.commands.message;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import wtf.beatrice.hidekobot.objects.MessageCommand;
import java.util.Arrays;
import java.util.LinkedList;
public class HelloCommand implements MessageCommand
{
@Override
public LinkedList<String> getCommandLabels() {
return new LinkedList<>(Arrays.asList("hi", "hello", "heya"));
}
@Override
public boolean passRawArgs() {
return false;
}
@Override
public void runCommand(MessageReceivedEvent event, String label, String[] args)
{
String senderId = event.getMessage().getAuthor().getId();
event.getMessage().reply("Hi, <@" + senderId + ">! :sparkles:").queue();
}
}

View File

@ -0,0 +1,104 @@
package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.objects.MessageCommand;
import wtf.beatrice.hidekobot.objects.MessageCommandAliasesComparator;
import wtf.beatrice.hidekobot.util.Logger;
import java.util.*;
public class MessageCommandListener extends ListenerAdapter
{
// map storing command labels and command object alphabetically.
private final TreeMap<LinkedList<String>, MessageCommand> registeredCommands =
new TreeMap<LinkedList<String>, MessageCommand>(new MessageCommandAliasesComparator());
private final String commandRegex = "(?i)^(hideko|hde)\\b";
// (?i) -> case insensitive flag
// ^ -> start of string (not in middle of a sentence)
// \b -> the word has to end here
// .* -> there can be anything else after this word
public void registerCommand(MessageCommand command)
{
registeredCommands.put(command.getCommandLabels(), command);
}
public MessageCommand getRegisteredCommand(String label)
{
for(LinkedList<String> aliases : registeredCommands.keySet())
{
for(String currentAlias : aliases)
{
if(label.equals(currentAlias))
{ return registeredCommands.get(aliases); }
}
}
return null;
}
public LinkedList<MessageCommand> getRegisteredCommands()
{ return new LinkedList<>(registeredCommands.values()); }
private final Logger logger = new Logger(MessageCommandListener.class);
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event)
{
String eventMessage = event.getMessage().getContentDisplay();
if(!eventMessage.toLowerCase().matches(commandRegex + ".*"))
return;
MessageChannel channel = event.getChannel();
// generate args from the string
String argsString = eventMessage.replaceAll(commandRegex + "\\s*", "");
// if no args were specified apart from the bot prefix
// note: we can't check argsRaw's size because String.split returns an array of size 1 if no match is found,
// and that element is the whole string passed as a single argument, which would be empty in this case
// (or contain text in other cases like "string split ," if the passed text doesn't contain any comma ->
// it will be the whole text as a single element.
if(argsString.isEmpty())
{
event.getMessage().reply("Hello there! ✨").queue();
return;
}
// split all passed arguments
String[] argsRaw = argsString.split("\\s+");
// extract the command that the user is trying to run
String commandLabel = argsRaw[0];
MessageCommand commandObject = getRegisteredCommand(commandLabel);
if(commandObject != null)
{
String[] commandArgs;
if(commandObject.passRawArgs())
{
// remove first argument, which is the command label
argsString = argsString.replaceAll("^[\\S]+\\s+", "");
// pass all other arguments as a single argument as the first array element
commandArgs = new String[]{argsString};
}
else
{
// copy all split arguments to the array, except from the command label
commandArgs = Arrays.copyOfRange(argsRaw, 1, argsRaw.length);
}
commandObject.runCommand(event, commandLabel, commandArgs);
} else {
event.getMessage().reply("Unrecognized command: `" + commandLabel + "`!").queue(); // todo prettier
}
}
}

View File

@ -1,64 +0,0 @@
package wtf.beatrice.hidekobot.listeners;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.jetbrains.annotations.NotNull;
import wtf.beatrice.hidekobot.Cache;
import wtf.beatrice.hidekobot.util.Logger;
public class MessageListener extends ListenerAdapter
{
private final String commandRegex = "(?i)^(hideko|hde)\\b";
// (?i) -> case insensitive flag
// ^ -> start of string (not in middle of a sentence)
// \b -> the word has to end here
// .* -> there can be anything else after this word
private final Logger logger = new Logger(MessageListener.class);
@Override
public void onMessageReceived(@NotNull MessageReceivedEvent event)
{
String eventMessage = event.getMessage().getContentDisplay();
if(!eventMessage.toLowerCase().matches(commandRegex + ".*"))
return;
MessageChannel channel = event.getChannel();
// generate args from the string
String argsString = eventMessage.replaceAll(commandRegex + "\\s*", "");
String[] args = argsString.split("\\s+");
event.getMessage().reply("Hi").queue();
if(eventMessage.equalsIgnoreCase("hideko"))
{
channel.sendMessage("Hello there! ✨").queue();
return;
}
if(eventMessage.equalsIgnoreCase("ping"))
{
channel.sendMessage("Pong!").queue();
return;
}
if(eventMessage.equalsIgnoreCase("hideko verbose"))
{
boolean verbose = Cache.isVerbose();
String msg = verbose ? "off" : "on";
msg = "Turning verbosity " + msg + "!";
Cache.setVerbose(!verbose);
channel.sendMessage(msg).queue();
logger.log(msg);
return;
}
}
}

View File

@ -11,7 +11,8 @@ import java.util.TreeMap;
public class SlashCommandListener extends ListenerAdapter public class SlashCommandListener extends ListenerAdapter
{ {
TreeMap<String, SlashCommand> registeredCommands = new TreeMap<>(); // map storing command label and command object alphabetically.
private final TreeMap<String, SlashCommand> registeredCommands = new TreeMap<>();
public void registerCommand(SlashCommand command) public void registerCommand(SlashCommand command)
{ {
@ -20,9 +21,7 @@ public class SlashCommandListener extends ListenerAdapter
} }
public SlashCommand getRegisteredCommand(String label) public SlashCommand getRegisteredCommand(String label)
{ { return registeredCommands.get(label); }
return registeredCommands.get(label);
}
public LinkedList<SlashCommand> getRegisteredCommands() public LinkedList<SlashCommand> getRegisteredCommands()
{ return new LinkedList<>(registeredCommands.values()); } { return new LinkedList<>(registeredCommands.values()); }

View File

@ -2,24 +2,43 @@ package wtf.beatrice.hidekobot.objects;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import java.util.LinkedList;
public interface MessageCommand public interface MessageCommand
{ {
/** /**
* Get the command's label, which is used when determining if this is the correct command or not. * Get the command's label(s), which are used when determining if this is the correct command or not.
* The first label in the collection is considered the main command name. All other labels are considered
* command aliases.
* *
* @return the command label. * @return the command label.
*/ */
String getCommandName(); LinkedList<String> getCommandLabels();
/**
* Say if this command does its own text parsing, and tell the message listener if it should automatically
* split all arguments in separate entries of an array, or pass everything as the first entry of that array.
*
* This is better instead of getting the message contents from the event, because the message listener will
* still strip the bot prefix and command name from the args, but leave the rest untouched.
*
* @return the boolean being true if no parsing should be made by the command handler.
*/
boolean passRawArgs();
/** /**
* Run the command logic by parsing the event and replying accordingly. * Run the command logic by parsing the event and replying accordingly.
* *
*
* @param event the received message event. It should not be used for parsing message contents data as * @param event the received message event. It should not be used for parsing message contents data as
* the arguments already account for it in a better way. * the arguments already account for it in a better way.
*
* @param label the command label that was used, taken from all available command aliases.
*
* @param args a pre-formatted list of arguments, excluding the bot prefix and the command name. * @param args a pre-formatted list of arguments, excluding the bot prefix and the command name.
* This is useful because command logic won't have to change in case the bot prefix is changed, * This is useful because command logic won't have to change in case the bot prefix is changed,
* removed, or we switch to another method of triggering commands (ping, trigger words, ...). * removed, or we switch to another method of triggering commands (ping, trigger words, ...).
*/ */
void runCommand(MessageReceivedEvent event, String[] args); void runCommand(MessageReceivedEvent event, String label, String[] args);
} }

View File

@ -0,0 +1,19 @@
package wtf.beatrice.hidekobot.objects;
import java.util.Comparator;
import java.util.LinkedList;
/**
* This class gets two linked lists, and compares their first value alphabetically.
*/
public class MessageCommandAliasesComparator implements Comparator<LinkedList<String>> {
@Override
public int compare(LinkedList<String> linkedList, LinkedList<String> t1) {
if(linkedList.isEmpty()) return 0;
if(t1.isEmpty()) return 0;
return linkedList.get(0).compareTo(t1.get(0));
}
}

View File

@ -8,7 +8,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.CommandData; import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import net.dv8tion.jda.api.interactions.commands.build.Commands; import net.dv8tion.jda.api.interactions.commands.build.Commands;
import wtf.beatrice.hidekobot.HidekoBot; import wtf.beatrice.hidekobot.HidekoBot;
import wtf.beatrice.hidekobot.listeners.MessageListener; import wtf.beatrice.hidekobot.listeners.MessageCommandListener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -16,7 +16,7 @@ import java.util.List;
public class SlashCommandUtil public class SlashCommandUtil
{ {
private static final Logger logger = new Logger(MessageListener.class); private static final Logger logger = new Logger(MessageCommandListener.class);
static List<CommandData> allCommands = new ArrayList<>() static List<CommandData> allCommands = new ArrayList<>()
{{ {{