diff --git a/README.md b/README.md index 50fb1e0153..95394b8d3b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ First add the jitpack repository: jitpack.io https://jitpack.io - + ``` Then add the dependency: ```xml diff --git a/application/src/main/java/org/togetherjava/tjbot/Application.java b/application/src/main/java/org/togetherjava/tjbot/Application.java index 95349819e4..fd9851607c 100644 --- a/application/src/main/java/org/togetherjava/tjbot/Application.java +++ b/application/src/main/java/org/togetherjava/tjbot/Application.java @@ -66,9 +66,7 @@ public static void runBot(String token, Path databasePath) { Database database = new Database("jdbc:sqlite:" + databasePath.toAbsolutePath()); JDA jda = JDABuilder.createDefault(token) - .addEventListeners(new PingPongListener()) - .addEventListeners(new DatabaseListener(database)) - .addEventListeners(new CommandHandler()) + .addEventListeners(new CommandHandler(database)) .build(); jda.awaitReady(); logger.info("Bot is ready"); diff --git a/application/src/main/java/org/togetherjava/tjbot/DatabaseListener.java b/application/src/main/java/org/togetherjava/tjbot/DatabaseListener.java deleted file mode 100644 index 9d94dac995..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/DatabaseListener.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.togetherjava.tjbot; - -import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.entities.ChannelType; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.togetherjava.tjbot.db.Database; -import org.togetherjava.tjbot.db.DatabaseException; -import org.togetherjava.tjbot.db.generated.tables.Storage; -import org.togetherjava.tjbot.db.generated.tables.records.StorageRecord; - -import java.util.Optional; - -/** - * Implementation of an example command to illustrate how to use a database. - *

- * The implemented commands are {@code !dbput} and {@code !dbget}. They act like some sort of simple - * {@code Map}, allowing the user to store and retrieve key-value pairs from the - * database. - *

- * For example: - * - *

- * {@code
- * !dbput hello Hello World!
- * // TJ-Bot: Saved under 'hello'.
- * !dbget hello
- * // TJ-Bot: Saved message: Hello World!
- * }
- * 
- */ -// TODO: Remove this class after #127 has been merged -public final class DatabaseListener extends ListenerAdapter { - - private static final Logger logger = LoggerFactory.getLogger(DatabaseListener.class); - - private final Database database; - - /** - * Creates a new command listener, using the given database. - * - * @param database the database to store the key-value pairs in - */ - public DatabaseListener(Database database) { - this.database = database; - } - - @Override - public void onMessageReceived(MessageReceivedEvent event) { - if (event.getAuthor().isBot()) { - return; - } - if (!event.isFromType(ChannelType.TEXT)) { - return; - } - if (event.isWebhookMessage()) { - return; - } - if (event.isFromType(ChannelType.PRIVATE)) { - return; - } - if (!event.getMember().hasPermission(Permission.MESSAGE_MANAGE)) { - return; - } - - String message = event.getMessage().getContentDisplay(); - if (message.startsWith("!dbput")) { - handlePutMessage(message, event); - } else if (message.startsWith("!dbget")) { - handleGetMessage(message, event); - } - } - - /** - * Handler for the {@code !dbput} command. - *

- * If the message is in the wrong format, it will respond to the user instead of throwing any - * exceptions. - * - * @param message the message to react to. For example {@code !dbput hello Hello World!}. - * @param event the event the message belongs to, mainly used to respond back to the user - */ - private void handlePutMessage(String message, MessageReceivedEvent event) { - // !dbput hello Hello World! - logger.info("#{}: Received '!dbput' command", event.getResponseNumber()); - String[] data = message.split(" ", 3); - if (data.length != 3) { - event.getChannel() - .sendMessage("Sorry, your message was in the wrong format, try '!dbput key value'") - .queue(); - return; - } - String key = data[1]; - String value = data[2]; - - try { - database.writeTransaction(ctx -> { - StorageRecord storageRecord = - ctx.newRecord(Storage.STORAGE).setKey(key).setValue(value); - if (storageRecord.update() == 0) { - storageRecord.insert(); - } - }); - - event.getChannel().sendMessage("Saved under '" + key + "'.").queue(); - } catch (DatabaseException e) { - logger.error("Failed to put message", e); - event.getChannel().sendMessage("Sorry, something went wrong.").queue(); - } - } - - /** - * Handler for the {@code !dbget} command. - *

- * If the message is in the wrong format, it will respond to the user instead of throwing any - * exceptions. - * - * @param message the message to react to. For example {@code !dbget hello}. - * @param event the event the message belongs to, mainly used to respond back to the user - */ - private void handleGetMessage(String message, MessageReceivedEvent event) { - // !dbget hello - logger.info("#{}: Received '!dbget' command", event.getResponseNumber()); - String[] data = message.split(" ", 2); - if (data.length != 2) { - event.getChannel() - .sendMessage("Sorry, your message was in the wrong format, try '!dbget key'") - .queue(); - return; - } - String key = data[1]; - - try { - // The lambda needs to be a block so the return type can distinguish between the two - // read/write methods. This feels like a sonar bug. - Optional foundValue = database.read(context -> { // NOSONAR - return Optional.ofNullable(context.selectFrom(Storage.STORAGE) - .where(Storage.STORAGE.KEY.eq(key)) - .fetchOne()); - }); - if (foundValue.isEmpty()) { - event.getChannel().sendMessage("Nothing found for the key '" + key + "'").queue(); - return; - } - - String value = foundValue.get().getValue(); - event.getChannel().sendMessage("Saved message: " + value).queue(); - } catch (DatabaseException e) { - logger.error("Failed to get message", e); - event.getChannel().sendMessage("Sorry, something went wrong.").queue(); - } - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/PingPongListener.java b/application/src/main/java/org/togetherjava/tjbot/PingPongListener.java deleted file mode 100644 index b173390f97..0000000000 --- a/application/src/main/java/org/togetherjava/tjbot/PingPongListener.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.togetherjava.tjbot; - -import net.dv8tion.jda.api.entities.ChannelType; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Implementation of an example command to illustrate how to use JDA. - *

- * The implemented command is {@code !ping}. The bot will respond with a message {@code Pong!}. - *

- * For example: - * - *

- * {@code
- * !ping
- * // TJ-Bot: Pong!
- * }
- * 
- */ -public final class PingPongListener extends ListenerAdapter { - - private static final Logger logger = LoggerFactory.getLogger(PingPongListener.class); - - /** - * Handler for the {@code !ping} command. Will ignore any message that is not exactly - * {@code !ping}. - * - * @param event the event the message belongs to - */ - @Override - public void onMessageReceived(MessageReceivedEvent event) { - if (event.getAuthor().isBot()) { - return; - } - if (!event.isFromType(ChannelType.TEXT)) { - return; - } - - if (!"!ping".equals(event.getMessage().getContentDisplay())) { - return; - } - - logger.info("#{}: Received '!ping' command", event.getResponseNumber()); - - event.getChannel().sendMessage("Pong!").queue(); - } -} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/CommandHandler.java b/application/src/main/java/org/togetherjava/tjbot/commands/CommandHandler.java index 982d03d1bf..178a56b1f8 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/CommandHandler.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/CommandHandler.java @@ -11,6 +11,9 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.basic.DatabaseCommand; +import org.togetherjava.tjbot.commands.basic.PingCommand; +import org.togetherjava.tjbot.db.Database; import java.util.ArrayList; import java.util.Arrays; @@ -22,7 +25,7 @@ /** * The command handler - * + *

* Commands need to be added to the commandList */ public class CommandHandler extends ListenerAdapter { @@ -31,8 +34,9 @@ public class CommandHandler extends ListenerAdapter { private final List commandList = new ArrayList<>(); private final Map commandMap; - public CommandHandler() { - commandList.addAll(List.of(new ReloadCommand(this))); + public CommandHandler(Database database) { + commandList.addAll( + List.of(new ReloadCommand(this), new PingCommand(), new DatabaseCommand(database))); commandMap = commandList.stream() .collect(Collectors.toMap(Command::getCommandName, Function.identity())); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/ReloadCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/ReloadCommand.java index f211bf3976..2ec7237e54 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/ReloadCommand.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/ReloadCommand.java @@ -78,7 +78,7 @@ public void onButtonClick(ButtonClickEvent event, List idArgs) { RestAction.allOf(restActions) .queue(updatedCommands -> event.getHook() .editOriginal( - "Commands successfully reloaded! *Global commands can take upto 1 hour to load*") + "Commands successfully reloaded! *Global commands can take up to 1 hour to load*") .queue()); } default -> event.reply("I am not sure what you clicked?") diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/DatabaseCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/DatabaseCommand.java new file mode 100644 index 0000000000..6250b479fb --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/DatabaseCommand.java @@ -0,0 +1,148 @@ +package org.togetherjava.tjbot.commands.basic; + +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.CommandInteraction; +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.SubcommandData; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.example.AbstractCommand; +import org.togetherjava.tjbot.db.Database; +import org.togetherjava.tjbot.db.DatabaseException; +import org.togetherjava.tjbot.db.generated.tables.Storage; +import org.togetherjava.tjbot.db.generated.tables.records.StorageRecord; + +import java.util.Objects; +import java.util.Optional; + +/** + * Implementation of an example command to illustrate how to use a database. + *

+ * The implemented command is {@code /db}. It has two subcommands {@code get} and {@code put}. It + * acts like some sort of simple {@code Map}, allowing the user to store and + * retrieve key-value pairs from the database. + *

+ * For example: + * + *

+ * {@code
+ * /db put hello Hello World!
+ * // TJ-Bot: Saved under 'hello'.
+ *
+ * /db get hello
+ * // TJ-Bot: Saved message: Hello World!
+ * }
+ * 
+ */ +public final class DatabaseCommand extends AbstractCommand { + private static final Logger logger = LoggerFactory.getLogger(DatabaseCommand.class); + private static final String GET_COMMAND = "get"; + private static final String PUT_COMMAND = "put"; + private static final String KEY_OPTION = "key"; + private static final String VALUE_OPTION = "value"; + private final Database database; + + /** + * Creates a new database command, using the given database. + * + * @param database the database to store the key-value pairs in + */ + public DatabaseCommand(Database database) { + super("db", "Storage and retrieval of key-value pairs", true); + this.database = database; + } + + @Override + public @NotNull CommandData addOptions(@NotNull CommandData commandData) { + return commandData.addSubcommands( + new SubcommandData(GET_COMMAND, + "Gets a value corresponding to a key from a database").addOption( + OptionType.STRING, KEY_OPTION, "the key of the value to retrieve", + true), + new SubcommandData(PUT_COMMAND, + "Puts a key-value pair into a database for later retrieval") + .addOption(OptionType.STRING, KEY_OPTION, + "the key of the value to save", true) + .addOption(OptionType.STRING, VALUE_OPTION, "the value to save", true)); + } + + @Override + public void onSlashCommand(SlashCommandEvent event) { + logger.info("#{}: Received '/db' command", event.getResponseNumber()); + + switch (Objects.requireNonNull(event.getSubcommandName())) { + case GET_COMMAND -> handleGetCommand(event); + case PUT_COMMAND -> handlePutCommand(event); + default -> throw new AssertionError(); + } + } + + /** + * Handles {@code /db get key} commands. Retrieves the value saved under the given key and + * responds with the results to the user. + * + * @param event the event of the command + */ + private void handleGetCommand(CommandInteraction event) { + // /db get hello + String key = Objects.requireNonNull(event.getOption(KEY_OPTION)).getAsString(); + try { + Optional value = database.read(context -> { + try (var select = context.selectFrom(Storage.STORAGE)) { + return Optional.ofNullable(select.where(Storage.STORAGE.KEY.eq(key)).fetchOne()) + .map(StorageRecord::getValue); + } + }); + if (value.isEmpty()) { + event.reply("Nothing found for the key '" + key + "'").setEphemeral(true).queue(); + return; + } + + event.reply("Saved message: " + value.orElseThrow()).queue(); + } catch (DatabaseException e) { + logger.error("Failed to get message", e); + event.reply("Sorry, something went wrong.").setEphemeral(true).queue(); + } + } + + /** + * Handles {@code /db put key value} commands. Saves the value under the given key and responds + * with the results to the user. + *

+ * This command can only be used by users with the {@code MESSAGE_MANAGE} permission. + * + * @param event the event of the command + */ + private void handlePutCommand(CommandInteraction event) { + // To prevent people from saving malicious content, only users with + // elevated permissions are allowed to use this command + if (!Objects.requireNonNull(event.getMember()).hasPermission(Permission.MESSAGE_MANAGE)) { + event.reply("You need the MESSAGE_MANAGE permission to use this command") + .setEphemeral(true) + .queue(); + return; + } + + // /db put hello Hello World! + String key = Objects.requireNonNull(event.getOption(KEY_OPTION)).getAsString(); + String value = Objects.requireNonNull(event.getOption(VALUE_OPTION)).getAsString(); + + try { + database.write(context -> { + StorageRecord storageRecord = + context.newRecord(Storage.STORAGE).setKey(key).setValue(value); + if (storageRecord.update() == 0) { + storageRecord.insert(); + } + }); + + event.reply("Saved under '" + key + "'.").queue(); + } catch (DatabaseException e) { + logger.error("Failed to put message", e); + event.reply("Sorry, something went wrong.").setEphemeral(true).queue(); + } + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/PingCommand.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/PingCommand.java new file mode 100644 index 0000000000..1a62b50668 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/PingCommand.java @@ -0,0 +1,29 @@ +package org.togetherjava.tjbot.commands.basic; + +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.togetherjava.tjbot.commands.example.AbstractCommand; + +/** + * Ping Pong command. + */ +public final class PingCommand extends AbstractCommand { + private static final Logger logger = LoggerFactory.getLogger(PingCommand.class); + + public PingCommand() { + super("ping", "Bot responds with 'Pong!'"); + } + + /** + * When the slash command is `/ping`, then the bot returns with the value `Pong!` + * + * @param event The relating {@link SlashCommandEvent} + */ + @Override + public void onSlashCommand(SlashCommandEvent event) { + logger.info("#{}: Received 'ping' command", event.getResponseNumber()); + + event.reply("Pong!").queue(); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/basic/package-info.java b/application/src/main/java/org/togetherjava/tjbot/commands/basic/package-info.java new file mode 100644 index 0000000000..3394e9bcb5 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/basic/package-info.java @@ -0,0 +1,5 @@ +/** + * This package offers some basic commands that act as example for how to use the command system of + * the application. + */ +package org.togetherjava.tjbot.commands.basic; diff --git a/application/src/test/resources/log4j2.xml b/application/src/test/resources/log4j2.xml new file mode 100644 index 0000000000..586ab0cc45 --- /dev/null +++ b/application/src/test/resources/log4j2.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file