Skip to content

Commit

Permalink
Modals and Text Input (#40)
Browse files Browse the repository at this point in the history
* Updating JDA to alpha.9

* Reverting missed <jar> change in pom.xml

* Fixing typo

* Adding space before casts to match Java formatting guidelines

* Modals Initial implementation

* Changes based on feedback

* Fixed registration order for DiscordModalCommand

* Error for Interaction already acknowledged
  • Loading branch information
TheHusseler committed May 19, 2022
1 parent 8fe9f04 commit dcc8d99
Show file tree
Hide file tree
Showing 6 changed files with 508 additions and 1 deletion.
8 changes: 7 additions & 1 deletion pom.xml
Expand Up @@ -59,7 +59,7 @@
<dependency>
<groupId>net.dv8tion</groupId>
<artifactId>JDA</artifactId>
<version>5.0.0-alpha.9</version>
<version>5.0.0-alpha.11</version>
</dependency>
</dependencies>

Expand Down Expand Up @@ -107,6 +107,8 @@
<include>org.apache.commons:**</include>
<include>org.slf4j:**</include>
<include>com.squareup.okio:**</include>
<include>org.jetbrains.kotlin:**</include>
<include>org.jetbrains:annotations:**</include>
<include>net.sf.trove4j:**</include>
</includes>
</artifactSet>
Expand Down Expand Up @@ -159,6 +161,10 @@
<pattern>org.slf4j</pattern>
<shadedPattern>com.denizenscript.shaded.org.slf4j</shadedPattern>
</relocation>
<relocation>
<pattern>org.jetbrains</pattern>
<shadedPattern>com.denizenscript.shaded.org.jetbrains</shadedPattern>
</relocation>
<relocation>
<pattern>okio</pattern>
<shadedPattern>com.denizenscript.shaded.okio</shadedPattern>
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/denizenscript/ddiscordbot/DenizenDiscordBot.java
Expand Up @@ -59,6 +59,7 @@ public void onEnable() {
DenizenCore.commandRegistry.registerCommand(DiscordCreateThreadCommand.class);
DenizenCore.commandRegistry.registerCommand(DiscordInteractionCommand.class);
DenizenCore.commandRegistry.registerCommand(DiscordMessageCommand.class);
DenizenCore.commandRegistry.registerCommand(DiscordModalCommand.class);
DenizenCore.commandRegistry.registerCommand(DiscordReactCommand.class);
// Events
ScriptEvent.registerScriptEvent(DiscordButtonClickedScriptEvent.class);
Expand All @@ -69,6 +70,7 @@ public void onEnable() {
ScriptEvent.registerScriptEvent(DiscordMessageReactionAddScriptEvent.class);
ScriptEvent.registerScriptEvent(DiscordMessageReactionRemoveScriptEvent.class);
ScriptEvent.registerScriptEvent(DiscordMessageReceivedScriptEvent.class);
ScriptEvent.registerScriptEvent(DiscordModalSubmittedScriptEvent.class);
ScriptEvent.registerScriptEvent(DiscordSelectionUsedScriptEvent.class);
ScriptEvent.registerScriptEvent(DiscordSlashCommandScriptEvent.class);
ScriptEvent.registerScriptEvent(DiscordThreadArchivedScriptEvent.class);
Expand All @@ -89,6 +91,7 @@ public void onEnable() {
ObjectFetcher.registerWithObjectFetcher(DiscordReactionTag.class, DiscordReactionTag.tagProcessor);
ObjectFetcher.registerWithObjectFetcher(DiscordRoleTag.class, DiscordRoleTag.tagProcessor);
ObjectFetcher.registerWithObjectFetcher(DiscordSelectionTag.class, DiscordSelectionTag.tagProcessor);
ObjectFetcher.registerWithObjectFetcher(DiscordTextInputTag.class, DiscordTextInputTag.tagProcessor);
ObjectFetcher.registerWithObjectFetcher(DiscordUserTag.class, DiscordUserTag.tagProcessor);
// Extension properties
PropertyParser.registerProperty(DiscordTimeTagProperties.class, TimeTag.class);
Expand Down Expand Up @@ -257,6 +260,21 @@ public void onEnable() {
return DiscordSelectionTag.valueOf(attribute.getParam(), attribute.context);
});
// <--[tag]
// @attribute <discord_text_input[(<button>)]>
// @returns DiscordTextInputTag
// @plugin dDiscordBot
// @description
// Returns a blank DiscordTextInputTag instance, to be filled with data via the with.as tag.
// Or, if given an input, returns a Discord TextInput object constructed from the input value.
// Refer to <@link objecttype DiscordTextInputTag>.
// -->
TagManager.registerTagHandler(DiscordTextInputTag.class, "discord_text_input", (attribute) -> {
if (!attribute.hasParam()) {
return new DiscordTextInputTag();
}
return DiscordTextInputTag.valueOf(attribute.getParam(), attribute.context);
});
// <--[tag]
// @attribute <discord_user[<user>]>
// @returns DiscordUserTag
// @plugin dDiscordBot
Expand Down
Expand Up @@ -14,6 +14,7 @@
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleAddEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRoleRemoveEvent;
import net.dv8tion.jda.api.events.guild.member.update.GuildMemberUpdateNicknameEvent;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
Expand Down Expand Up @@ -136,6 +137,11 @@ public void onButtonInteraction(ButtonInteractionEvent event) {
autoHandle(event, DiscordButtonClickedScriptEvent.instance);
}

@Override
public void onModalInteraction(ModalInteractionEvent event) {
autoHandle(event, DiscordModalSubmittedScriptEvent.instance);
}

@Override
public void onSelectMenuInteraction(SelectMenuInteractionEvent event) {
autoHandle(event, DiscordSelectionUsedScriptEvent.instance);
Expand Down
@@ -0,0 +1,121 @@
package com.denizenscript.ddiscordbot.commands;

import com.denizenscript.ddiscordbot.DenizenDiscordBot;
import com.denizenscript.ddiscordbot.objects.*;
import com.denizenscript.denizencore.objects.ObjectTag;
import com.denizenscript.denizencore.objects.core.ElementTag;
import com.denizenscript.denizencore.scripts.ScriptEntry;
import com.denizenscript.denizencore.scripts.commands.Holdable;
import com.denizenscript.denizencore.utilities.CoreUtilities;
import com.denizenscript.denizencore.utilities.debugging.Debug;
import net.dv8tion.jda.api.interactions.callbacks.IModalCallback;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
import net.dv8tion.jda.api.interactions.components.Modal;
import net.dv8tion.jda.api.requests.restaction.interactions.ModalCallbackAction;
import org.bukkit.Bukkit;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class DiscordModalCommand extends AbstractDiscordCommand implements Holdable {

public DiscordModalCommand() {
setName("discordmodal");
setSyntax("discordmodal [interaction:<interaction>] [name:<name>] [rows:<rows>] (title:<title>)");
setRequiredArguments(3, 4);
setPrefixesHandled("interaction", "rows", "title", "name");
isProcedural = false;
}

// <--[command]
// @Name discordmodal
// @Syntax discordmodal [interaction:<interaction>] [name:<name>] [rows:<rows>] (title:<title>)
// @Required 3
// @Maximum 4
// @Short Manages Discord modals.
// @Plugin dDiscordBot
// @Guide https://guide.denizenscript.com/guides/expanding/ddiscordbot.html
// @Group external
//
// With this command you can respond to an interaction using a modal.
//
// You should usually defer an interaction before using a modal.
//
// The command should usually be ~waited for. See <@link language ~waitable>.
//
// @Usage
// Use to create a modal from text replies.
// - definemap rows:
// 1:
// 1: <discord_text_input.with[name].as[textinput].with[label].as[Type here!].with[style].as[short]>
// - ~discordmodal interaction:<context.interaction> name:example_modal title:Modal! rows:<[rows]>
//
// -->

@Override
public void execute(ScriptEntry scriptEntry) {
DiscordInteractionTag interaction = scriptEntry.requiredArgForPrefix("interaction", DiscordInteractionTag.class);
ElementTag name = scriptEntry.requiredArgForPrefixAsElement("name");
ElementTag title = scriptEntry.argForPrefixAsElement("title", "");
ObjectTag rows = scriptEntry.requiredArgForPrefix("rows", ObjectTag.class);
if (scriptEntry.dbCallShouldDebug()) {
Debug.report(scriptEntry, getName(), interaction, name, rows, title);
}
Runnable runner = () -> {
try {
if (interaction.interaction == null) {
handleError(scriptEntry, "Invalid interaction! Has it expired?");
return;
}
List<ActionRow> actionRows = createRows(scriptEntry, rows);
if(actionRows == null || actionRows.isEmpty()) {
handleError(scriptEntry, "Invalid action rows!");
return;
}
if (!interaction.interaction.isAcknowledged()) {
IModalCallback replyTo = (IModalCallback) interaction.interaction;

Modal modal = Modal.create(name.toString(), title.toString())
.addActionRows(actionRows)
.build();

ModalCallbackAction action = replyTo.replyModal(modal);
action.complete();
} else {
handleError(scriptEntry, "Interaction already acknowledged!");
return;
}
}
catch (Exception ex) {
handleError(scriptEntry, ex);
}
};
Bukkit.getScheduler().runTaskAsynchronously(DenizenDiscordBot.instance, () -> {
runner.run();
scriptEntry.setFinished(true);
});
}

public static List<ActionRow> createRows(ScriptEntry scriptEntry, ObjectTag rowsObj) {
if (rowsObj == null) {
return null;
}
Collection<ObjectTag> rows = CoreUtilities.objectToList(rowsObj, scriptEntry.getContext());
List<ActionRow> actionRows = new ArrayList<>();
for (ObjectTag row : rows) {
List<ItemComponent> components = new ArrayList<>();
for (ObjectTag component : CoreUtilities.objectToList(row, scriptEntry.getContext())) {
if (component.canBeType(DiscordTextInputTag.class)) {
components.add(component.asType(DiscordTextInputTag.class, scriptEntry.getContext()).build());
}
else {
Debug.echoError("Unsupported modal component list entry '" + component + "'");
}
}
actionRows.add(ActionRow.of(components));
}
return actionRows;
}
}
@@ -0,0 +1,94 @@
package com.denizenscript.ddiscordbot.events;

import com.denizenscript.ddiscordbot.DiscordScriptEvent;
import com.denizenscript.ddiscordbot.objects.DiscordChannelTag;
import com.denizenscript.ddiscordbot.objects.DiscordGroupTag;
import com.denizenscript.ddiscordbot.objects.DiscordInteractionTag;
import com.denizenscript.ddiscordbot.objects.DiscordSelectionTag;
import com.denizenscript.denizencore.objects.ObjectTag;
import com.denizenscript.denizencore.objects.core.ElementTag;
import com.denizenscript.denizencore.objects.core.MapTag;
import com.denizenscript.denizencore.utilities.text.StringHolder;
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent;

import java.util.Map;
import java.util.stream.Collectors;

public class DiscordModalSubmittedScriptEvent extends DiscordScriptEvent {

// <--[event]
// @Events
// discord modal submitted
//
// @Switch for:<bot> to only process the event for a specified Discord bot.
// @Switch channel:<channel_id> to only process the event when it occurs in a specified Discord channel.
// @Switch group:<group_id> to only process the event for a specified Discord group.
// @Switch name:<modal_name> to only process the event for a specified Discord modal.
//
// @Triggers when a Discord user submits a modal.
//
// @Plugin dDiscordBot
//
// @Group Discord
//
// @Context
// <context.bot> returns the relevant DiscordBotTag.
// <context.channel> returns the DiscordChannelTag.
// <context.group> returns the DiscordGroupTag.
// <context.interaction> returns the DiscordInteractionTag.
// <context.values> returns a MapTag of the values submitted by the user.
//
// -->

public static DiscordModalSubmittedScriptEvent instance;

public DiscordModalSubmittedScriptEvent() {
instance = this;
registerCouldMatcher("discord modal submitted");
registerSwitches("channel", "group", "name");
}

public ModalInteractionEvent getEvent() {
return (ModalInteractionEvent) event;
}

@Override
public boolean matches(ScriptPath path) {
if (!tryChannel(path, getEvent().getChannel())) {
return false;
}
if (!tryGuild(path, getEvent().isFromGuild() ? getEvent().getGuild() : null)) {
return false;
}
if (!runGenericSwitchCheck(path, "name", getEvent().getModalId())) {
return false;
}
return super.matches(path);
}

@Override
public ObjectTag getContext(String name) {
switch (name) {
case "channel":
return new DiscordChannelTag(botID, getEvent().getChannel());
case "group":
if (getEvent().isFromGuild()) {
return new DiscordGroupTag(botID, getEvent().getGuild());
}
break;
case "interaction":
return DiscordInteractionTag.getOrCreate(botID, getEvent().getInteraction());
case "values":
Map<StringHolder, ObjectTag> map = getEvent().getValues().stream()
.collect(Collectors.toMap(key -> new StringHolder(key.getId()), value -> new ElementTag(value.getAsString(), true)));
return new MapTag(map);
}
return super.getContext(name);
}

@Override
public String getName() {
return "DiscordModalSubmitted";
}
}

0 comments on commit dcc8d99

Please sign in to comment.