diff --git a/src/main/java/net/javadiscord/javabot2/Bot.java b/src/main/java/net/javadiscord/javabot2/Bot.java index efa52bb..2e9294b 100644 --- a/src/main/java/net/javadiscord/javabot2/Bot.java +++ b/src/main/java/net/javadiscord/javabot2/Bot.java @@ -8,6 +8,7 @@ import com.zaxxer.hikari.HikariDataSource; import lombok.extern.slf4j.Slf4j; import net.javadiscord.javabot2.command.SlashCommandListener; +import net.javadiscord.javabot2.command.interaction.InteractionListener; import net.javadiscord.javabot2.config.BotConfig; import net.javadiscord.javabot2.db.DbHelper; import net.javadiscord.javabot2.systems.moderation.ModerationService; @@ -81,9 +82,20 @@ public static void main(String[] args) { "commands/moderation.yaml" ); api.addSlashCommandCreateListener(commandListener); + addEventListeners(api); initScheduledTasks(api); } + /** + * Adds all the bot's event listeners to the Javacord instance, except for the + * main {@link SlashCommandListener}. + * @param api the {@link DiscordApi} instance to add listeners to. + */ + private static void addEventListeners(DiscordApi api) { + api.addButtonClickListener(new InteractionListener()); + api.addSelectMenuChooseListener(new InteractionListener()); + } + /** * Initializes all the basic data sources that are needed by the bot's other * capabilities. This should be called before logging in diff --git a/src/main/java/net/javadiscord/javabot2/command/interaction/InteractionHandlerBuilder.java b/src/main/java/net/javadiscord/javabot2/command/interaction/InteractionHandlerBuilder.java new file mode 100644 index 0000000..8c1cf0e --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/interaction/InteractionHandlerBuilder.java @@ -0,0 +1,53 @@ +package net.javadiscord.javabot2.command.interaction; + +import net.javadiscord.javabot2.command.interaction.button.ButtonAction; +import net.javadiscord.javabot2.command.interaction.selection_menu.SelectMenuAction; +import org.javacord.api.entity.message.component.ActionRow; +import org.javacord.api.entity.message.component.LowLevelComponent; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class that is used to build message interactions. + */ +public class InteractionHandlerBuilder { + private final InteractionImmediateResponseBuilder responseBuilder; + private final List buttons = new ArrayList<>(); + private final List selectMenus = new ArrayList<>(); + + public InteractionHandlerBuilder(InteractionImmediateResponseBuilder responseBuilder) { + this.responseBuilder = responseBuilder; + } + + /** + * Adds one or multiple {@link ButtonAction}s. + * @param buttonActions an array of {@link ButtonAction}s. + * @return the current instance in order to allow chain call methods. + */ + public InteractionHandlerBuilder addButtons(ButtonAction... buttonActions) { + for (var action : buttonActions) buttons.add(action.getButton()); + return this; + } + + /** + * Adds one or multiple {@link SelectMenuAction}s. + * @param selectMenuActions an array of {@link SelectMenuAction}s. + * @return the current instance in order to allow chain call methods. + */ + public InteractionHandlerBuilder addSelectionMenus(SelectMenuAction... selectMenuActions) { + for (var action : selectMenuActions) { + selectMenus.add(action.getSelectMenu()); + } + return this; + } + + /** + * Returns the provided Response Builder. + * @return the current instance in order to allow chain call methods. + */ + public InteractionImmediateResponseBuilder getResponseBuilder() { + return responseBuilder.addComponents(ActionRow.of(buttons), ActionRow.of(selectMenus)); + } +} diff --git a/src/main/java/net/javadiscord/javabot2/command/interaction/InteractionListener.java b/src/main/java/net/javadiscord/javabot2/command/interaction/InteractionListener.java new file mode 100644 index 0000000..676ad73 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/interaction/InteractionListener.java @@ -0,0 +1,57 @@ +package net.javadiscord.javabot2.command.interaction; + +import net.javadiscord.javabot2.command.ResponseException; +import net.javadiscord.javabot2.command.interaction.button.ButtonHandler; +import net.javadiscord.javabot2.command.interaction.selection_menu.SelectionMenuHandler; +import org.javacord.api.event.interaction.ButtonClickEvent; +import org.javacord.api.event.interaction.SelectMenuChooseEvent; +import org.javacord.api.listener.interaction.ButtonClickListener; +import org.javacord.api.listener.interaction.SelectMenuChooseListener; + +import java.lang.reflect.InvocationTargetException; + +/** + * Listener for all supported interactions. + */ +public class InteractionListener implements ButtonClickListener, SelectMenuChooseListener { + + @Override + public void onButtonClick(ButtonClickEvent event) { + var id = event.getButtonInteraction().getCustomId().split(":"); + ButtonHandler handler = (ButtonHandler) getHandlerByName(id[0]); + try { + handler.handleButtonInteraction(event.getButtonInteraction()).respond(); + } catch (ResponseException e) { + e.printStackTrace(); + } + } + + @Override + public void onSelectMenuChoose(SelectMenuChooseEvent event) { + var id = event.getSelectMenuInteraction().getCustomId().split(":"); + SelectionMenuHandler handler = (SelectionMenuHandler) getHandlerByName(id[0]); + try { + handler.handleSelectMenuInteraction(event.getSelectMenuInteraction()).respond(); + } catch (ResponseException e) { + e.printStackTrace(); + } + } + + /** + * Tries to get a Class by the specified name + * (Class paths are baked into the button id) and returns it. + * @param name the class name + * @return The handler class + */ + private Object getHandlerByName(String name) { + try { + return Class.forName(name) + .getDeclaredConstructor() + .newInstance(); + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | + IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/net/javadiscord/javabot2/command/interaction/button/ButtonAction.java b/src/main/java/net/javadiscord/javabot2/command/interaction/button/ButtonAction.java new file mode 100644 index 0000000..18374bd --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/interaction/button/ButtonAction.java @@ -0,0 +1,148 @@ +package net.javadiscord.javabot2.command.interaction.button; + +import org.javacord.api.entity.emoji.Emoji; +import org.javacord.api.entity.message.component.Button; +import org.javacord.api.entity.message.component.ButtonBuilder; +import org.javacord.api.entity.message.component.ButtonStyle; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class that represents a single Button Interaction. + */ +public class ButtonAction { + + private final String label; + private final ButtonStyle buttonStyle; + private Class handler; + private final List params; + private boolean disabled = false; + private String url; + private Emoji emoji; + + /** + * Constructor of the Button Action. + * @param label the button's label + * @param buttonStyle the button's {@link ButtonStyle} + */ + public ButtonAction(String label, ButtonStyle buttonStyle) { + this.params = new ArrayList<>(); + this.label = label; + this.buttonStyle = buttonStyle; + } + + /** + * Sets a handler class for this button interaction. + * The class must extend {@link ButtonHandler}. + * @param handler a class that should handle the button interaction. + * @return the current instance in order to allow chain call methods. + */ + public ButtonAction handledBy(Class handler) { + this.handler = handler; + return this; + } + + /** + * Adds a parameter, which is later baked into the button id. + * @param param an object that is later converted to a string to bake it into the button id as a parameter. + * @return the current instance in order to allow chain call methods. + */ + public ButtonAction addParam(Object param) { + params.add(String.valueOf(param)); + return this; + } + + /** + * Enables/Disables the button based on the provided boolean. + * @param disabled the boolean which enables/disables the button. + * @return the current instance in order to allow chain call methods. + */ + public ButtonAction setDisabled(boolean disabled) { + this.disabled = disabled; + return this; + } + + /** + * Sets an Url for this button. + * This only works for buttons with the {@link ButtonStyle#LINK} button style. + * @param url the url as a string. + * @return the current instance in order to allow chain call methods. + */ + public ButtonAction setUrl(String url) { + this.url = url; + return this; + } + + /** + * Sets an Emoji for this button. + * @param emoji the emoji which should be used. + * @return the current instance in order to allow chain call methods. + */ + public ButtonAction setEmoji(Emoji emoji) { + this.emoji = emoji; + return this; + } + + /** + * Returns the compiled button id. Every parameter is seperated with a colon. + * The first argument is always the handler's class path. + * After that, all set params ({@link ButtonAction#addParam(Object)}) get appended to the id. + * @return the compiled button id. + */ + public String getCustomId() { + StringBuilder id = new StringBuilder(handler.getName()); + for (String param : params) { + id.append(":").append(param); + } + return id.toString(); + } + + /** + * Returns the button's {@link ButtonStyle}. + * @return the button's {@link ButtonStyle}. + */ + public ButtonStyle getButtonStyle() { + return buttonStyle; + } + + /** + * Returns the button's label. + * @return the button's label + */ + public String getLabel() { + return label; + } + + /** + * Returns the button's handler class. + * @return The handler class + */ + public Class getHandler() { + return handler; + } + + /** + * Returns a list with all parameters of the button. + * @return A List with all parameters. + */ + public List getParams() { + return params; + } + + /** + * Returns the complete button. + * @return the compiled button. + */ + public Button getButton() { + var builder = new ButtonBuilder() + .setLabel(label) + .setStyle(buttonStyle) + .setDisabled(disabled); + if (buttonStyle != ButtonStyle.LINK) builder.setCustomId(getCustomId()); + if (url != null && url.length() > 0 && buttonStyle == ButtonStyle.LINK) builder.setUrl(url); + if (emoji != null) builder.setEmoji(emoji); + + return builder.build(); + } +} diff --git a/src/main/java/net/javadiscord/javabot2/command/interaction/button/ButtonHandler.java b/src/main/java/net/javadiscord/javabot2/command/interaction/button/ButtonHandler.java new file mode 100644 index 0000000..d94bd5f --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/interaction/button/ButtonHandler.java @@ -0,0 +1,13 @@ +package net.javadiscord.javabot2.command.interaction.button; + +import net.javadiscord.javabot2.command.ResponseException; +import org.javacord.api.interaction.ButtonInteraction; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +/** + * An interface that should be implemented by any class that is utilizing + * button interactions. + */ +public interface ButtonHandler { + InteractionImmediateResponseBuilder handleButtonInteraction(ButtonInteraction interaction) throws ResponseException; +} diff --git a/src/main/java/net/javadiscord/javabot2/command/interaction/selection_menu/SelectMenuAction.java b/src/main/java/net/javadiscord/javabot2/command/interaction/selection_menu/SelectMenuAction.java new file mode 100644 index 0000000..da97923 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/interaction/selection_menu/SelectMenuAction.java @@ -0,0 +1,142 @@ +package net.javadiscord.javabot2.command.interaction.selection_menu; + +import org.javacord.api.entity.message.component.SelectMenu; +import org.javacord.api.entity.message.component.SelectMenuBuilder; +import org.javacord.api.entity.message.component.SelectMenuOption; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Class that represents a single Select Menu Interaction. + */ +public class SelectMenuAction { + + private Class handler; + private List options; + private String placeholder; + private int minValue = 0; + private int maxValue = 25; + private boolean disabled = false; + + /** + * Constructor of the Select Menu Action. + */ + public SelectMenuAction() { + this.options = new ArrayList<>(); + } + + /** + * Sets a handler class for this select menu interaction. + * The class must extend {@link SelectionMenuHandler}. + * @param handler a class that should handle the select menu interaction. + * @return the current instance in order to allow chain call methods. + */ + public SelectMenuAction handledBy(Class handler) { + this.handler = handler; + return this; + } + + /** + * Sets the select menu's placeholder text. + * @param placeholder the select menu's placeholder. + * @return the current instance in order to allow chain call methods. + */ + public SelectMenuAction setPlaceholder(String placeholder) { + this.placeholder = placeholder; + return this; + } + + /** + * Enables/Disables the select menu based on the provided boolean. + * @param disabled the boolean which enables/disables the select menu. + * @return the current instance in order to allow chain call methods. + */ + public SelectMenuAction setDisabled(boolean disabled) { + this.disabled = disabled; + return this; + } + + /** + * Sets the min value for select menu options. + * @param minValue the min value for select menu options. + * @return the current instance in order to allow chain call methods. + */ + public SelectMenuAction setMinValue(int minValue) { + this.minValue = minValue; + return this; + } + + /** + * Sets the max value for select menu options. + * @param maxValue the max value for select menu options. + * @return the current instance in order to allow chain call methods. + */ + public SelectMenuAction setMaxValue(int maxValue) { + this.maxValue = maxValue; + return this; + } + + /** + * Adds a single option to the select menu. + * @param option a single {@link SelectMenuOption}. + * @return the current instance in order to allow chain call methods. + */ + public SelectMenuAction addOption(SelectMenuOption option) { + options.add(option); + return this; + } + + /** + * Adds multiple options to the select menu. + * @param options an array with all {@link SelectMenuOption}s. + * @return the current instance in order to allow chain call methods. + */ + public SelectMenuAction addOptions(SelectMenuOption... options) { + this.options.addAll(Arrays.asList(options)); + return this; + } + + /** + * Returns the select menu's custom id. + * (which is just the path of the handler class.) + * @return the custom id. + */ + public String getCustomId() { + return handler.getName(); + } + + /** + * Returns the select menu's handler class. + * @return the select menu's handler class. + */ + public Class getHandler() { + return handler; + } + + /** + * Returns a list with all options. + * @return a list with all options. + */ + public List getOptions() { + return options; + } + + /** + * Returns the complete select menu. + * @return the complete select menu. + */ + public SelectMenu getSelectMenu() { + if (options.isEmpty()) throw new IllegalStateException("SelectMenu options may not be empty!"); + if (minValue > maxValue) throw new IllegalArgumentException("minValue may not be greater than maxValue!"); + var builder = new SelectMenuBuilder() + .setCustomId(getCustomId()) + .setMinimumValues(minValue) + .setMaximumValues(Math.min(maxValue, options.size())) // maxValue cannot be greater than the provided amount of options + .addOptions(options) + .setDisabled(disabled); + if (placeholder != null && placeholder.length() > 0) builder.setPlaceholder(placeholder); + return builder.build(); + } +} diff --git a/src/main/java/net/javadiscord/javabot2/command/interaction/selection_menu/SelectionMenuHandler.java b/src/main/java/net/javadiscord/javabot2/command/interaction/selection_menu/SelectionMenuHandler.java new file mode 100644 index 0000000..254ed29 --- /dev/null +++ b/src/main/java/net/javadiscord/javabot2/command/interaction/selection_menu/SelectionMenuHandler.java @@ -0,0 +1,13 @@ +package net.javadiscord.javabot2.command.interaction.selection_menu; + +import net.javadiscord.javabot2.command.ResponseException; +import org.javacord.api.interaction.SelectMenuInteraction; +import org.javacord.api.interaction.callback.InteractionImmediateResponseBuilder; + +/** + * An interface that should be implemented by any class that is utilizing + * select menu interactions. + */ +public interface SelectionMenuHandler { + InteractionImmediateResponseBuilder handleSelectMenuInteraction(SelectMenuInteraction interaction) throws ResponseException; +}