Skip to content
This repository was archived by the owner on Dec 1, 2021. It is now read-only.

Interaction Framework #19

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main/java/net/javadiscord/javabot2/Bot.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <strong>before</strong> logging in
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LowLevelComponent> buttons = new ArrayList<>();
private final List<LowLevelComponent> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends ButtonHandler> handler;
private final List<String> 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a check to make sure that the label does not contain the ':' character. Throw an IllegalArgumentException if so, since this will mess up handling the interaction later on.

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<? extends ButtonHandler> 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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, check that the param's string representation cannot contain the ':' symbol. Also it would probably be nicer to just require a String parameter instead of accepting any object and then converting it into a string. Also this doesn't seem to support the notion of type-checked or named parameters, which is something that would make handling the button interactions much easier, since then each developer who adds an interaction wouldn't be responsible for their own manual parsing and serializing of possibly complex data.

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<? extends ButtonHandler> getHandler() {
return handler;
}

/**
* Returns a list with all parameters of the button.
* @return A List with all parameters.
*/
public List<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Loading