From 9ce455262d0bdf76d56d0f00065e24557e81ede2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= <4096670+Citymonstret@users.noreply.github.com> Date: Sun, 14 Jan 2024 21:25:44 +0100 Subject: [PATCH] feat(jda): add parsers for JDA types (#1) The parsers wrap the JDA option mappings. --- .../cloud/discord/jda5/CommandListener.java | 9 +- .../discord/jda5/JDA5CommandManager.java | 7 + .../cloud/discord/jda5/JDAInteraction.java | 20 +++ .../cloud/discord/jda5/JDAOptionType.java | 79 +++++++++++ .../incendo/cloud/discord/jda5/JDAParser.java | 130 ++++++++++++++++++ .../jda5/StandardJDACommandFactory.java | 7 + 6 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAOptionType.java create mode 100644 cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAParser.java diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/CommandListener.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/CommandListener.java index 4f394de..b050958 100644 --- a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/CommandListener.java +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/CommandListener.java @@ -71,6 +71,7 @@ public void onSlashCommandInteraction(final @NonNull SlashCommandInteractionEven .guild(event.getGuild()) .replyCallback(event) .interactionEvent(event) + .addAllOptionMappings(event.getOptions()) .build(); this.commandManager.commandExecutor().executeCommand( this.commandManager.senderMapper().map(interaction), @@ -93,6 +94,7 @@ public void onCommandAutoCompleteInteraction(final @NonNull CommandAutoCompleteI .guild(event.getGuild()) .replyCallback(null) .interactionEvent(null) + .addAllOptionMappings(event.getOptions()) .build(); event.replyChoices( this.commandManager.suggestionFactory().suggestImmediately( @@ -119,7 +121,12 @@ public void onCommandAutoCompleteInteraction(final @NonNull CommandAutoCompleteI private @NonNull String extractCommandName(final @NonNull CommandInteractionPayload payload) { final StringBuilder command = new StringBuilder(payload.getFullCommandName()); payload.getOptions().forEach(option -> { - command.append(" ").append(option.getAsString()); + command.append(" "); + if (JDAOptionType.JDA_TYPES.stream().anyMatch(type -> type.value() == option.getType().getKey())) { + command.append(option.getName()); + } else { + command.append(option.getAsString()); + } }); return command.toString(); } diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDA5CommandManager.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDA5CommandManager.java index 4623f83..aa78d7e 100644 --- a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDA5CommandManager.java +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDA5CommandManager.java @@ -90,6 +90,13 @@ public JDA5CommandManager( this.discordSettings.set(DiscordSetting.AUTO_REGISTER_SLASH_COMMANDS, true); this.registerDefaultExceptionHandlers(); + + this.parserRegistry() + .registerParser(JDAParser.userParser()) + .registerParser(JDAParser.roleParser()) + .registerParser(JDAParser.channelParser()) + .registerParser(JDAParser.mentionableParser()) + .registerParser(JDAParser.attachmentParser()); } @Override diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAInteraction.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAInteraction.java index 1f7d25d..f4ef376 100644 --- a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAInteraction.java +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAInteraction.java @@ -23,10 +23,13 @@ // package org.incendo.cloud.discord.jda5; +import java.util.List; +import java.util.Optional; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.GenericCommandInteractionEvent; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -75,6 +78,23 @@ public interface JDAInteraction { */ @Nullable IReplyCallback replyCallback(); + /** + * Returns the raw JDA option mappings. + * + * @return option mappings + */ + @NonNull List<@NonNull OptionMapping> optionMappings(); + + /** + * Returns the option mapping with the given {@code key}, if it exists. + * + * @param key mapping key + * @return the mapping + */ + default @NonNull Optional<@NonNull OptionMapping> getOptionMapping(final @NonNull String key) { + return this.optionMappings().stream().filter(mapping -> mapping.getName().equalsIgnoreCase(key)).findFirst(); + } + /** * Maps between {@link JDAInteraction} and {@link C}. diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAOptionType.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAOptionType.java new file mode 100644 index 0000000..b4e65df --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAOptionType.java @@ -0,0 +1,79 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import io.leangen.geantyref.TypeToken; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import net.dv8tion.jda.api.entities.IMentionable; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.Channel; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.discord.slash.DiscordOptionType; + +/** + * Extension of {@link DiscordOptionType} for JDA-specific classes. + * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public final class JDAOptionType { + + public static final @NonNull DiscordOptionType USER = DiscordOptionType.of( + "USER", + 6, + TypeToken.get(User.class) + ); + public static final @NonNull DiscordOptionType CHANNEL = DiscordOptionType.of( + "CHANNEL", + 7, + TypeToken.get(Channel.class) + ); + public static final @NonNull DiscordOptionType ROLE = DiscordOptionType.of( + "ROLE", + 8, + TypeToken.get(Role.class) + ); + public static final @NonNull DiscordOptionType MENTIONABLE = DiscordOptionType.of( + "MENTIONABLE", + 9, + TypeToken.get(IMentionable.class) + ); + public static final @NonNull DiscordOptionType ATTACHMENT = DiscordOptionType.of( + "ATTACHMENT", + 11, + TypeToken.get(Message.Attachment.class) + ); + + public static final Collection<@NonNull DiscordOptionType> JDA_TYPES = Collections.unmodifiableCollection( + Arrays.asList(USER, CHANNEL, ROLE, MENTIONABLE, ATTACHMENT) + ); + + private JDAOptionType() { + } +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAParser.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAParser.java new file mode 100644 index 0000000..88cef95 --- /dev/null +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/JDAParser.java @@ -0,0 +1,130 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package org.incendo.cloud.discord.jda5; + +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.arguments.parser.ParserDescriptor; +import cloud.commandframework.context.CommandContext; +import cloud.commandframework.context.CommandInput; +import net.dv8tion.jda.api.entities.IMentionable; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.Channel; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * A parser which wraps a JDA {@link OptionMapping}. + * + * @param command sender type + * @param JDA type + * @since 1.0.0 + */ +@FunctionalInterface +@API(status = API.Status.STABLE, since = "1.0.0") +public interface JDAParser extends ArgumentParser { + + /** + * Returns a parser which extracts a {@link User}. + * + * @param command sender type + * @return the parser + */ + static @NonNull ParserDescriptor userParser() { + return ParserDescriptor.of((JDAParser) mapping -> ArgumentParseResult.success(mapping.getAsUser()), User.class); + } + + /** + * Returns a parser which extracts a {@link Channel}. + * + * @param command sender type + * @return the parser + */ + static @NonNull ParserDescriptor channelParser() { + return ParserDescriptor.of( + (JDAParser) mapping -> ArgumentParseResult.success(mapping.getAsChannel()), + Channel.class + ); + } + + /** + * Returns a parser which extracts a {@link Role}. + * + * @param command sender type + * @return the parser + */ + static @NonNull ParserDescriptor roleParser() { + return ParserDescriptor.of( + (JDAParser) mapping -> ArgumentParseResult.success(mapping.getAsRole()), + Role.class + ); + } + + /** + * Returns a parser which extracts a {@link IMentionable}. + * + * @param command sender type + * @return the parser + */ + static @NonNull ParserDescriptor mentionableParser() { + return ParserDescriptor.of( + (JDAParser) mapping -> ArgumentParseResult.success(mapping.getAsMentionable()), + IMentionable.class + ); + } + + /** + * Returns a parser which extracts an {@link Message.Attachment}. + * + * @param command sender type + * @return the parser + */ + static @NonNull ParserDescriptor attachmentParser() { + return ParserDescriptor.of( + (JDAParser) mapping -> ArgumentParseResult.success(mapping.getAsAttachment()), + Message.Attachment.class + ); + } + + /** + * Returns the result of extracting the argument from the given {@code mapping}. + * + * @param mapping JDA option mapping + * @return the result + */ + @NonNull ArgumentParseResult extract(@NonNull OptionMapping mapping); + + @Override + default @NonNull ArgumentParseResult<@NonNull T> parse( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull CommandInput commandInput + ) { + final JDAInteraction interaction = commandContext.get(JDA5CommandManager.CONTEXT_JDA_INTERACTION); + final OptionMapping mapping = interaction.getOptionMapping(commandInput.readString()).orElse(null); + return this.extract(mapping); + } +} diff --git a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/StandardJDACommandFactory.java b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/StandardJDACommandFactory.java index 23fdb7b..cafa848 100644 --- a/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/StandardJDACommandFactory.java +++ b/cloud-jda5/src/main/java/org/incendo/cloud/discord/jda5/StandardJDACommandFactory.java @@ -67,6 +67,13 @@ public StandardJDACommandFactory(final @NonNull CommandTree commandTree) { this.commandTree = commandTree; final OptionRegistry optionRegistry = new StandardOptionRegistry<>(); + optionRegistry + .registerMapping(JDAOptionType.USER, JDAParser.userParser()) + .registerMapping(JDAOptionType.CHANNEL, JDAParser.channelParser()) + .registerMapping(JDAOptionType.ROLE, JDAParser.roleParser()) + .registerMapping(JDAOptionType.MENTIONABLE, JDAParser.mentionableParser()) + .registerMapping(JDAOptionType.ATTACHMENT, JDAParser.attachmentParser()); + this.discordCommandFactory = new StandardDiscordCommandFactory<>(optionRegistry); this.nodeProcessor = new NodeProcessor<>(this.commandTree);