diff --git a/library/build.gradle b/library/build.gradle index 180a547..6d8ac47 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -7,7 +7,7 @@ plugins { } group = 'net.stelitop' -version = '0.0.4' +version = '0.0.5' java { sourceCompatibility = '17' diff --git a/library/src/main/java/net/stelitop/mad4j/DiscordEventsComponent.java b/library/src/main/java/net/stelitop/mad4j/DiscordEventsComponent.java index b4b4abf..f17f238 100644 --- a/library/src/main/java/net/stelitop/mad4j/DiscordEventsComponent.java +++ b/library/src/main/java/net/stelitop/mad4j/DiscordEventsComponent.java @@ -1,7 +1,7 @@ package net.stelitop.mad4j; import net.stelitop.mad4j.commands.SlashCommand; -import net.stelitop.mad4j.components.ComponentInteraction; +import net.stelitop.mad4j.commands.components.ComponentInteraction; import org.springframework.stereotype.Component; import java.lang.annotation.ElementType; diff --git a/library/src/main/java/net/stelitop/mad4j/Mad4jConfig.java b/library/src/main/java/net/stelitop/mad4j/Mad4jConfig.java index d8eeb99..1dbbb5d 100644 --- a/library/src/main/java/net/stelitop/mad4j/Mad4jConfig.java +++ b/library/src/main/java/net/stelitop/mad4j/Mad4jConfig.java @@ -1,19 +1,7 @@ package net.stelitop.mad4j; -import discord4j.core.GatewayDiscordClient; -import net.stelitop.mad4j.autocomplete.AutocompletionExecutor; -import net.stelitop.mad4j.listeners.CommandOptionAutocompleteListener; -import net.stelitop.mad4j.listeners.ComponentEventListener; -import net.stelitop.mad4j.listeners.SlashCommandListener; -import net.stelitop.mad4j.requirements.CommandRequirementExecutor; -import org.checkerframework.checker.units.qual.C; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; - -import java.util.List; /** *

Configuration for mad4j.

diff --git a/library/src/main/java/net/stelitop/mad4j/commands/Command.java b/library/src/main/java/net/stelitop/mad4j/commands/Command.java new file mode 100644 index 0000000..a77e3a4 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/commands/Command.java @@ -0,0 +1,30 @@ +package net.stelitop.mad4j.commands; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

Annotation for all commands to be used by the bot.

+ */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Command { + /** + * The name of the command. + */ + String name(); + /** + * The description of the command. + */ + String description(); + /** + * The types this command registers as. Can be multiple types. The existing types + * are "text" for commands from messages used a prefix, and "slash" for slash commands. + * If no types are given, then the default type is slash commands only, unless changed + * in the properties file. + */ + // TODO: Replace these with enum types + CommandType[] types() default {}; +} diff --git a/library/src/main/java/net/stelitop/mad4j/commands/CommandData.java b/library/src/main/java/net/stelitop/mad4j/commands/CommandData.java new file mode 100644 index 0000000..7deac39 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/commands/CommandData.java @@ -0,0 +1,115 @@ +package net.stelitop.mad4j.commands; + +import discord4j.core.GatewayDiscordClient; +import lombok.Builder; +import lombok.Getter; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.listeners.CommandOptionAutocompleteListener; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Collectors; + +@Component +@Order(0) +public class CommandData implements ApplicationRunner { + + private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + + private final GatewayDiscordClient gatewayDiscordClient; + private final CommandOptionAutocompleteListener commandOptionAutocompleteListener; + private final ApplicationContext applicationContext; + private final Environment environment; + private final List commandsInfo = new ArrayList<>(); + + private static final Set DEFAULT_COMMAND_TYPES = Set.of(CommandType.Slash); + + @Builder + @Getter + public static class Entry { + private String name; + private String description; + private Set types; + private Object bean; + private Method method; + } + + @Autowired + public CommandData( + GatewayDiscordClient gatewayDiscordClient, + CommandOptionAutocompleteListener commandOptionAutocompleteListener, + ApplicationContext applicationContext, + Environment environment + ) { + this.gatewayDiscordClient = gatewayDiscordClient; + this.commandOptionAutocompleteListener = commandOptionAutocompleteListener; + this.applicationContext = applicationContext; + this.environment = environment; + } + + @Override + public void run(ApplicationArguments args) { + Collection commandBeans = applicationContext.getBeansWithAnnotation(DiscordEventsComponent.class).values(); + + commandsInfo.clear(); + for (var bean : commandBeans) { + for (var method : bean.getClass().getMethods()) { + Entry data = getCommandData(bean, method); + if (data == null) continue; + commandsInfo.add(data); + } + } + // TODO: Verify no overlap + } + + private Entry getCommandData(Object bean, Method method) { + if (method.isAnnotationPresent(Command.class)) { + Command c = method.getAnnotation(Command.class); + return Entry.builder() + .name(c.name().toLowerCase()) + .description(c.description()) + .types(getCommandTypes(c.types())) + .bean(bean) + .method(method) + .build(); + } else if (method.isAnnotationPresent(SlashCommand.class)) { + SlashCommand sc = method.getAnnotation(SlashCommand.class); + return Entry.builder() + .name(sc.name().toLowerCase()) + .description(sc.description()) + .types(Set.of(CommandType.Slash)) + .bean(bean) + .method(method) + .build(); + } + return null; + } + + private Set getCommandTypes(CommandType[] types) { + if (types.length == 0) return DEFAULT_COMMAND_TYPES; + else return Arrays.stream(types).collect(Collectors.toSet()); + } + + public @Nullable Entry get(String commandName, CommandType type) { + String nameLower = commandName.toLowerCase(); + return commandsInfo.stream() + .filter(x -> x.name.toLowerCase().equals(nameLower) && x.types.contains(type)) + .findFirst() + .orElse(null); + } + public List getFromType(CommandType type) { + return commandsInfo.stream() + .filter(x -> x.types.contains(type)) + .toList(); + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/commands/CommandParam.java b/library/src/main/java/net/stelitop/mad4j/commands/CommandParam.java index 5ca0436..c2fd875 100644 --- a/library/src/main/java/net/stelitop/mad4j/commands/CommandParam.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/CommandParam.java @@ -1,7 +1,7 @@ package net.stelitop.mad4j.commands; -import net.stelitop.mad4j.autocomplete.AutocompletionExecutor; -import net.stelitop.mad4j.autocomplete.NullAutocompleteExecutor; +import net.stelitop.mad4j.commands.autocomplete.AutocompletionExecutor; +import net.stelitop.mad4j.commands.autocomplete.NullAutocompleteExecutor; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/library/src/main/java/net/stelitop/mad4j/commands/CommandType.java b/library/src/main/java/net/stelitop/mad4j/commands/CommandType.java new file mode 100644 index 0000000..84426a3 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/commands/CommandType.java @@ -0,0 +1,6 @@ +package net.stelitop.mad4j.commands; + +public enum CommandType { + Text, + Slash +} diff --git a/library/src/main/java/net/stelitop/mad4j/InteractionEvent.java b/library/src/main/java/net/stelitop/mad4j/commands/InteractionEvent.java similarity index 91% rename from library/src/main/java/net/stelitop/mad4j/InteractionEvent.java rename to library/src/main/java/net/stelitop/mad4j/commands/InteractionEvent.java index d04cd14..f176ef9 100644 --- a/library/src/main/java/net/stelitop/mad4j/InteractionEvent.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/InteractionEvent.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j; +package net.stelitop.mad4j.commands; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/library/src/main/java/net/stelitop/mad4j/SlashCommandRegistrar.java b/library/src/main/java/net/stelitop/mad4j/commands/SlashCommandRegistrar.java similarity index 80% rename from library/src/main/java/net/stelitop/mad4j/SlashCommandRegistrar.java rename to library/src/main/java/net/stelitop/mad4j/commands/SlashCommandRegistrar.java index 195ee11..2a40ec3 100644 --- a/library/src/main/java/net/stelitop/mad4j/SlashCommandRegistrar.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/SlashCommandRegistrar.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j; +package net.stelitop.mad4j.commands; import discord4j.core.GatewayDiscordClient; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; @@ -7,17 +7,13 @@ import discord4j.discordjson.json.ApplicationCommandOptionData; import discord4j.discordjson.json.ApplicationCommandRequest; import discord4j.discordjson.json.ImmutableApplicationCommandOptionData; -import discord4j.discordjson.possible.Possible; import discord4j.rest.RestClient; import lombok.AllArgsConstructor; import lombok.ToString; -import net.stelitop.mad4j.autocomplete.NullAutocompleteExecutor; -import net.stelitop.mad4j.commands.CommandParam; -import net.stelitop.mad4j.commands.CommandParamChoice; -import net.stelitop.mad4j.commands.DefaultValue; -import net.stelitop.mad4j.commands.SlashCommand; -import net.stelitop.mad4j.convenience.EventUser; -import net.stelitop.mad4j.convenience.EventUserId; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.commands.autocomplete.NullAutocompleteExecutor; +import net.stelitop.mad4j.commands.convenience.EventUser; +import net.stelitop.mad4j.commands.convenience.EventUserId; import net.stelitop.mad4j.listeners.CommandOptionAutocompleteListener; import net.stelitop.mad4j.utils.ActionResult; import net.stelitop.mad4j.utils.OptionType; @@ -26,11 +22,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; -import org.springframework.context.ApplicationContext; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; -import javax.management.RuntimeErrorException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; @@ -56,20 +50,20 @@ public class SlashCommandRegistrar implements ApplicationRunner { private final GatewayDiscordClient gatewayDiscordClient; private final CommandOptionAutocompleteListener commandOptionAutocompleteListener; - private final ApplicationContext applicationContext; private final Environment environment; + private final CommandData commandData; @Autowired public SlashCommandRegistrar( GatewayDiscordClient gatewayDiscordClient, CommandOptionAutocompleteListener commandOptionAutocompleteListener, - ApplicationContext applicationContext, - Environment environment + Environment environment, + CommandData commandData ) { this.gatewayDiscordClient = gatewayDiscordClient; this.commandOptionAutocompleteListener = commandOptionAutocompleteListener; - this.applicationContext = applicationContext; this.environment = environment; + this.commandData = commandData; } /** @@ -89,19 +83,9 @@ public SlashCommandRegistrar( @Override public void run(ApplicationArguments args) { - var slashCommandMethods = applicationContext.getBeansWithAnnotation(DiscordEventsComponent.class).values().stream() - .map(Object::getClass) - .map(Class::getMethods) - .flatMap(Arrays::stream) - .filter(m -> m.isAnnotationPresent(SlashCommand.class)) - .toList(); - - String missingCommandEventAnnotation = slashCommandMethods.stream() - .filter(x -> Arrays.stream(x.getParameters()).noneMatch(y -> y.isAnnotationPresent(InteractionEvent.class))) - .map(Method::getName) - .collect(Collectors.joining(", ")); + var slashCommands = commandData.getFromType(CommandType.Slash); - List> failedMethodVerifications = slashCommandMethods.stream() + List> failedMethodVerifications = slashCommands.stream() .map(this::verifySlashCommandMethodSignature) .filter(ActionResult::hasFailed) .toList(); @@ -115,11 +99,7 @@ public void run(ApplicationArguments args) { throw new RuntimeException(errorMsg + " Check the error logs for more detail on what went wrong."); } - if (!missingCommandEventAnnotation.isBlank()) { - throw new RuntimeException("Following commands don't have a command event: " + missingCommandEventAnnotation); - } - - var slashCommandRequests = createCommandRequestsFromMethods(slashCommandMethods); + var slashCommandRequests = createCommandRequestsFromCommandData(slashCommands); String updateCommands = Optional.ofNullable(environment.getProperty("mad4j.slashcommands.update")).orElse("true"); if (updateCommands.equalsIgnoreCase("false")) { @@ -145,13 +125,13 @@ public void run(ApplicationArguments args) { * of the commands a tree is created to group commands that have the same first * names. * - * @param methods List of methods annotated with {@link SlashCommand}. + * @param commandEntries List of methods annotated with {@link SlashCommand}. * @return A list of application command requests. Every request is about a * different command. */ - private List createCommandRequestsFromMethods(List methods) { + private List createCommandRequestsFromCommandData(List commandEntries) { - List trees = createCommandNameTrees(methods); + List trees = createCommandNameTrees(commandEntries); return trees.stream() .map(this::processSlashCommandTree) @@ -164,18 +144,15 @@ private List createCommandRequestsFromMethods(List createCommandNameTrees(List methods) { + private List createCommandNameTrees(List commandEntries) { CommandTreeNode shadowRoot = new CommandTreeNode("", new ArrayList<>(), null); - for (var method : methods) { - SlashCommand scAnnotation = method.getAnnotation(SlashCommand.class); - if (scAnnotation == null) continue; - - String commandName = scAnnotation.name().toLowerCase(); + for (var entry : commandEntries) { + String commandName = entry.getName(); String[] parts = commandName.split(" "); if (parts.length == 0) continue; @@ -186,11 +163,11 @@ private List createCommandNameTrees(List methods) { currentNode.children.add(new CommandTreeNode(partName, new ArrayList<>(), null)); } else if (i == parts.length - 1) { - throw new RuntimeException("The command \"" + scAnnotation.name().toLowerCase() + "\" has been declared multiple times!"); + throw new RuntimeException("The command \"" + entry.getName() + "\" has been declared multiple times!"); } currentNode = currentNode.children.stream().filter(x -> x.name.equals(partName)).findFirst().get(); } - currentNode.method = method; + currentNode.commandData = entry; } return shadowRoot.children; @@ -211,9 +188,9 @@ private static class CommandTreeNode { */ public List children; /** - * The method that would execute the slash command, if this is a leaf. + * The command data of the slash command, if this is a leaf. */ - public Method method; + public CommandData.Entry commandData; } /** @@ -226,13 +203,9 @@ private static class CommandTreeNode { private ApplicationCommandRequest processSlashCommandTree(CommandTreeNode tree) { var requestBuilder = ApplicationCommandRequest.builder(); requestBuilder.name(tree.name); - if (tree.method != null) { - var annotation = tree.method.getAnnotation(SlashCommand.class); - if (annotation == null) { - return null; - } - requestBuilder.description(annotation.description()); - requestBuilder.addAllOptions(getOptionsFromMethod(tree.method)); + if (tree.commandData != null) { + requestBuilder.description(tree.commandData.getDescription()); + requestBuilder.addAllOptions(getOptionsFromMethod(tree.commandData)); return requestBuilder.build(); } requestBuilder.description("Description for " + tree.name); @@ -251,14 +224,10 @@ private ApplicationCommandOptionData createOptionFromTreeChild(CommandTreeNode n // this is a method var acodBuilder = ApplicationCommandOptionData.builder(); acodBuilder.name(node.name); - if (node.method != null) { - var annotation = node.method.getAnnotation(SlashCommand.class); - if (annotation == null) { - return ApplicationCommandOptionData.builder().build(); - } - acodBuilder.description(annotation.description()); + if (node.commandData != null) { + acodBuilder.description(node.commandData.getDescription()); acodBuilder.type(OptionType.SUB_COMMAND); - acodBuilder.addAllOptions(getOptionsFromMethod(node.method)); + acodBuilder.addAllOptions(getOptionsFromMethod(node.commandData)); return acodBuilder.build(); } acodBuilder.description("Description for " + node.name); @@ -272,16 +241,16 @@ private ApplicationCommandOptionData createOptionFromTreeChild(CommandTreeNode n * {@link ApplicationCommandOptionData} objects for the slash command request. These * are the parameters of the slash command. * - * @param method Method to parse. + * @param commandData Method to parse. * @return A list of {@link ApplicationCommandOptionData} for the annotated methods. */ - private List getOptionsFromMethod(Method method) { + private List getOptionsFromMethod(CommandData.Entry commandData) { List ret = new ArrayList<>(); - var parameters = Arrays.stream(method.getParameters()) + var parameters = Arrays.stream(commandData.getMethod().getParameters()) .filter(x -> x.isAnnotationPresent(CommandParam.class)) .toList(); - String commandName = method.getAnnotation(SlashCommand.class).name().toLowerCase(); + String commandName = commandData.getName(); for (var parameter : parameters) { CommandParam paramAnnotation = parameter.getAnnotation(CommandParam.class); @@ -313,7 +282,7 @@ private ApplicationCommandOptionData parseRegularCommandParam( // For Autocomplete: // TODO: Check that there are no options available for the command // TODO: Check that the type of the input is one of String, Number or Integer - if (annotation.autocomplete()!= NullAutocompleteExecutor.class) { + if (annotation.autocomplete() != NullAutocompleteExecutor.class) { acodBuilder.autocomplete(true); String paramName = annotation.name().toLowerCase(); commandOptionAutocompleteListener.addMapping(commandName, paramName, annotation.autocomplete()); @@ -363,16 +332,16 @@ private void addChoicesToCommandParam( * the name of the command annotation and the parameter annotations and them having correct * data types.

* - * @param method The method to verify. + * @param commandData The method to verify. * @return An action result that if correct is empty. */ - private ActionResult verifySlashCommandMethodSignature(Method method) { - SlashCommand sca = method.getAnnotation(SlashCommand.class); - if (sca == null) return ActionResult.fail("The slash command with signature " + method + " did not have a @SlashCommand annotation!"); - if (sca.name().split(" ").length > 3) return ActionResult.fail("The slash command name " + sca.name() + " has too many parts! Maximum 3."); + private ActionResult verifySlashCommandMethodSignature(CommandData.Entry commandData) { + if (commandData.getName().split(" ").length > 3) { + return ActionResult.fail("The slash command name " + commandData.getName() + " has too many parts! Maximum 3."); + } - for (Parameter par : method.getParameters()) { - String msgStart = "Parameter \"" + par + "\" of method \"" + method + "\" "; + for (Parameter par : commandData.getMethod().getParameters()) { + String msgStart = "Parameter \"" + par + "\" of method \"" + commandData.getMethod() + "\" "; List> presentAnnotations = new ArrayList<>(); if (par.isAnnotationPresent(CommandParam.class)) { presentAnnotations.add(CommandParam.class); diff --git a/library/src/main/java/net/stelitop/mad4j/autocomplete/AutocompletionExecutor.java b/library/src/main/java/net/stelitop/mad4j/commands/autocomplete/AutocompletionExecutor.java similarity index 94% rename from library/src/main/java/net/stelitop/mad4j/autocomplete/AutocompletionExecutor.java rename to library/src/main/java/net/stelitop/mad4j/commands/autocomplete/AutocompletionExecutor.java index ca670de..ef1a001 100644 --- a/library/src/main/java/net/stelitop/mad4j/autocomplete/AutocompletionExecutor.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/autocomplete/AutocompletionExecutor.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j.autocomplete; +package net.stelitop.mad4j.commands.autocomplete; import discord4j.core.event.domain.interaction.ChatInputAutoCompleteEvent; import net.stelitop.mad4j.commands.CommandParam; diff --git a/library/src/main/java/net/stelitop/mad4j/autocomplete/InputSuggestion.java b/library/src/main/java/net/stelitop/mad4j/commands/autocomplete/InputSuggestion.java similarity index 91% rename from library/src/main/java/net/stelitop/mad4j/autocomplete/InputSuggestion.java rename to library/src/main/java/net/stelitop/mad4j/commands/autocomplete/InputSuggestion.java index 63bf9b1..54ce434 100644 --- a/library/src/main/java/net/stelitop/mad4j/autocomplete/InputSuggestion.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/autocomplete/InputSuggestion.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j.autocomplete; +package net.stelitop.mad4j.commands.autocomplete; import lombok.Getter; import org.jetbrains.annotations.NotNull; diff --git a/library/src/main/java/net/stelitop/mad4j/autocomplete/NullAutocompleteExecutor.java b/library/src/main/java/net/stelitop/mad4j/commands/autocomplete/NullAutocompleteExecutor.java similarity index 87% rename from library/src/main/java/net/stelitop/mad4j/autocomplete/NullAutocompleteExecutor.java rename to library/src/main/java/net/stelitop/mad4j/commands/autocomplete/NullAutocompleteExecutor.java index 0143a41..bd1414d 100644 --- a/library/src/main/java/net/stelitop/mad4j/autocomplete/NullAutocompleteExecutor.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/autocomplete/NullAutocompleteExecutor.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j.autocomplete; +package net.stelitop.mad4j.commands.autocomplete; import discord4j.core.event.domain.interaction.ChatInputAutoCompleteEvent; diff --git a/library/src/main/java/net/stelitop/mad4j/components/ComponentInteraction.java b/library/src/main/java/net/stelitop/mad4j/commands/components/ComponentInteraction.java similarity index 90% rename from library/src/main/java/net/stelitop/mad4j/components/ComponentInteraction.java rename to library/src/main/java/net/stelitop/mad4j/commands/components/ComponentInteraction.java index cadbcdb..fbe9626 100644 --- a/library/src/main/java/net/stelitop/mad4j/components/ComponentInteraction.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/components/ComponentInteraction.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j.components; +package net.stelitop.mad4j.commands.components; import discord4j.core.event.domain.interaction.ComponentInteractionEvent; import org.intellij.lang.annotations.RegExp; diff --git a/library/src/main/java/net/stelitop/mad4j/convenience/EventUser.java b/library/src/main/java/net/stelitop/mad4j/commands/convenience/EventUser.java similarity index 91% rename from library/src/main/java/net/stelitop/mad4j/convenience/EventUser.java rename to library/src/main/java/net/stelitop/mad4j/commands/convenience/EventUser.java index a3697e4..6847d18 100644 --- a/library/src/main/java/net/stelitop/mad4j/convenience/EventUser.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/convenience/EventUser.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j.convenience; +package net.stelitop.mad4j.commands.convenience; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/library/src/main/java/net/stelitop/mad4j/convenience/EventUserId.java b/library/src/main/java/net/stelitop/mad4j/commands/convenience/EventUserId.java similarity index 91% rename from library/src/main/java/net/stelitop/mad4j/convenience/EventUserId.java rename to library/src/main/java/net/stelitop/mad4j/commands/convenience/EventUserId.java index ff7af37..9bfffd5 100644 --- a/library/src/main/java/net/stelitop/mad4j/convenience/EventUserId.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/convenience/EventUserId.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j.convenience; +package net.stelitop.mad4j.commands.convenience; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/library/src/main/java/net/stelitop/mad4j/requirements/CommandRequirement.java b/library/src/main/java/net/stelitop/mad4j/commands/requirements/CommandRequirement.java similarity index 96% rename from library/src/main/java/net/stelitop/mad4j/requirements/CommandRequirement.java rename to library/src/main/java/net/stelitop/mad4j/commands/requirements/CommandRequirement.java index e0b08da..0402c38 100644 --- a/library/src/main/java/net/stelitop/mad4j/requirements/CommandRequirement.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/requirements/CommandRequirement.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j.requirements; +package net.stelitop.mad4j.commands.requirements; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/library/src/main/java/net/stelitop/mad4j/requirements/CommandRequirementExecutor.java b/library/src/main/java/net/stelitop/mad4j/commands/requirements/CommandRequirementExecutor.java similarity index 94% rename from library/src/main/java/net/stelitop/mad4j/requirements/CommandRequirementExecutor.java rename to library/src/main/java/net/stelitop/mad4j/commands/requirements/CommandRequirementExecutor.java index 52427d7..85e6187 100644 --- a/library/src/main/java/net/stelitop/mad4j/requirements/CommandRequirementExecutor.java +++ b/library/src/main/java/net/stelitop/mad4j/commands/requirements/CommandRequirementExecutor.java @@ -1,4 +1,4 @@ -package net.stelitop.mad4j.requirements; +package net.stelitop.mad4j.commands.requirements; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; import net.stelitop.mad4j.utils.ActionResult; diff --git a/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/DMCommandRequirement.java b/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/DMCommandRequirement.java new file mode 100644 index 0000000..bcd7259 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/DMCommandRequirement.java @@ -0,0 +1,15 @@ +package net.stelitop.mad4j.commands.requirements.standard; + +import net.stelitop.mad4j.commands.requirements.CommandRequirement; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@CommandRequirement(implementation = DMCommandRequirementImplementation.class) +public @interface DMCommandRequirement { +} diff --git a/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/DMCommandRequirementImplementation.java b/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/DMCommandRequirementImplementation.java new file mode 100644 index 0000000..7585959 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/DMCommandRequirementImplementation.java @@ -0,0 +1,27 @@ +package net.stelitop.mad4j.commands.requirements.standard; + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.entity.channel.Channel; +import discord4j.core.object.entity.channel.MessageChannel; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.commands.requirements.CommandRequirement; +import net.stelitop.mad4j.commands.requirements.CommandRequirementExecutor; +import net.stelitop.mad4j.utils.ActionResult; +import org.springframework.stereotype.Component; + +@DiscordEventsComponent +public class DMCommandRequirementImplementation implements CommandRequirementExecutor { + @Override + public ActionResult verify(ChatInputInteractionEvent event) { + MessageChannel channel = event.getInteraction().getChannel().block(); + if (channel == null) { + throw new NullPointerException("Could not get the channel of an interaction!"); + } + boolean inPrivate = channel.getType().equals(Channel.Type.DM); + if (inPrivate) { + return ActionResult.success(); + } else { + return ActionResult.fail("This command only works in DMs!"); + } + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/GuildCommandRequirement.java b/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/GuildCommandRequirement.java new file mode 100644 index 0000000..a6edcda --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/GuildCommandRequirement.java @@ -0,0 +1,15 @@ +package net.stelitop.mad4j.commands.requirements.standard; + +import net.stelitop.mad4j.commands.requirements.CommandRequirement; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@CommandRequirement(implementation = GuildCommandRequirementImplementation.class) +public @interface GuildCommandRequirement { +} diff --git a/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/GuildCommandRequirementImplementation.java b/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/GuildCommandRequirementImplementation.java new file mode 100644 index 0000000..d2768a9 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/commands/requirements/standard/GuildCommandRequirementImplementation.java @@ -0,0 +1,21 @@ +package net.stelitop.mad4j.commands.requirements.standard; + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.commands.requirements.CommandRequirement; +import net.stelitop.mad4j.commands.requirements.CommandRequirementExecutor; +import net.stelitop.mad4j.utils.ActionResult; +import org.springframework.stereotype.Component; + +@DiscordEventsComponent +public class GuildCommandRequirementImplementation implements CommandRequirementExecutor { + @Override + public ActionResult verify(ChatInputInteractionEvent event) { + boolean inGuild = event.getInteraction().getGuildId().isPresent(); + if (inGuild) { + return ActionResult.success(); + } else { + return ActionResult.fail("This command only works in a server!"); + } + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/events/AllowedEventResult.java b/library/src/main/java/net/stelitop/mad4j/events/AllowedEventResult.java new file mode 100644 index 0000000..f0d234f --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/events/AllowedEventResult.java @@ -0,0 +1,26 @@ +package net.stelitop.mad4j.events; + +import discord4j.core.event.domain.Event; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * Takes care of different types of allowed responses for different types of events. + */ +public interface AllowedEventResult { + +// /** +// * Verifies that the response value is of the wanted type for this handler. +// * @param object Returned value +// * +// * @return True if the +// */ +// default boolean verify(Object object) { +// +// } + List> resultTypes(); + List> eventTypes(); + + Mono transform(Object result, Event event); +} diff --git a/library/src/main/java/net/stelitop/mad4j/events/AllowedEventResultHandler.java b/library/src/main/java/net/stelitop/mad4j/events/AllowedEventResultHandler.java new file mode 100644 index 0000000..fd4a3c3 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/events/AllowedEventResultHandler.java @@ -0,0 +1,49 @@ +package net.stelitop.mad4j.events; + +import discord4j.core.event.domain.Event; +import net.stelitop.mad4j.utils.ActionResult; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Component +public class AllowedEventResultHandler { + + private final List responses; + + @Autowired + public AllowedEventResultHandler(List responses) { + this.responses = responses; + verifyNoOverlap(); + } + + public ActionResult> handleEventResult(Object result, Event event) { + var allowedEventResult = getMatchingResult(result, event); + if (allowedEventResult == null) return ActionResult.fail("This event result type is not allowed!"); + return ActionResult.success(allowedEventResult.transform(result, event)); + } + + private AllowedEventResult getMatchingResult(Object result, Event event) { + for (var response : responses) { + if (response.resultTypes().stream().anyMatch(r -> r.isAssignableFrom(result.getClass())) + && response.eventTypes().stream().anyMatch(e -> e.isAssignableFrom(event.getClass()))) { + return response; + } + } + return null; + } + + private void verifyNoOverlap() { + for (int i = 0; i < responses.size(); i++) { + for (int j = i + 1; j < responses.size(); j++) { + var x = responses.get(i); + var y = responses.get(j); + + + // actually implement allowing multiple things + } + } + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/events/SlashCommandEmbedCreateSpecEventResult.java b/library/src/main/java/net/stelitop/mad4j/events/SlashCommandEmbedCreateSpecEventResult.java new file mode 100644 index 0000000..27c2aec --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/events/SlashCommandEmbedCreateSpecEventResult.java @@ -0,0 +1,30 @@ +package net.stelitop.mad4j.events; + +import discord4j.core.event.domain.Event; +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.spec.EmbedCreateSpec; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Component +public class SlashCommandEmbedCreateSpecEventResult implements AllowedEventResult { + @Override + public List> resultTypes() { + return List.of(EmbedCreateSpec.class); + } + + @Override + public List> eventTypes() { + return List.of(ChatInputInteractionEvent.class); + } + + @Override + public Mono transform(Object result, Event event) { + if (result instanceof EmbedCreateSpec r && event instanceof ChatInputInteractionEvent e) { + return e.reply().withEmbeds(r); + } + throw new IllegalArgumentException(); + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/events/SlashCommandEventResponseEventResult.java b/library/src/main/java/net/stelitop/mad4j/events/SlashCommandEventResponseEventResult.java new file mode 100644 index 0000000..ff65033 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/events/SlashCommandEventResponseEventResult.java @@ -0,0 +1,36 @@ +package net.stelitop.mad4j.events; + +import discord4j.core.event.domain.Event; +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.event.domain.interaction.InteractionCreateEvent; +import net.stelitop.mad4j.interactions.EventResponse; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Component +public class SlashCommandEventResponseEventResult implements AllowedEventResult { + @Override + public List> resultTypes() { + return List.of(EventResponse.class); + } + + @Override + public List> eventTypes() { + return List.of(InteractionCreateEvent.class); + } + + @Override + public Mono transform(Object result, Event event) { + if (!EventResponse.class.isAssignableFrom(result.getClass())) { + throw new IllegalArgumentException(); + } + if (!ChatInputInteractionEvent.class.isAssignableFrom(event.getClass())) { + throw new IllegalArgumentException(); + } + var r = (EventResponse) result; + ChatInputInteractionEvent e = (ChatInputInteractionEvent) event; + return r.respond(e); + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/events/SlashCommandMonoVoidEventResult.java b/library/src/main/java/net/stelitop/mad4j/events/SlashCommandMonoVoidEventResult.java new file mode 100644 index 0000000..ded82f5 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/events/SlashCommandMonoVoidEventResult.java @@ -0,0 +1,29 @@ +package net.stelitop.mad4j.events; + +import discord4j.core.event.domain.Event; +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Component +public class SlashCommandMonoVoidEventResult implements AllowedEventResult { + @Override + public List> resultTypes() { + return List.of(Mono.class); + } + + @Override + public List> eventTypes() { + return List.of(ChatInputInteractionEvent.class); + } + + @Override + public Mono transform(Object result, Event event) { + if (!Mono.class.isAssignableFrom(result.getClass())) { + throw new IllegalArgumentException(); + } + return ((Mono) result).cast(Void.class); + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/events/SlashCommandStringEventResult.java b/library/src/main/java/net/stelitop/mad4j/events/SlashCommandStringEventResult.java new file mode 100644 index 0000000..6502af6 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/events/SlashCommandStringEventResult.java @@ -0,0 +1,30 @@ +package net.stelitop.mad4j.events; + +import discord4j.core.event.domain.Event; +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Component +public class SlashCommandStringEventResult implements AllowedEventResult { + + @Override + public List> resultTypes() { + return List.of(String.class); + } + + @Override + public List> eventTypes() { + return List.of(ChatInputInteractionEvent.class); + } + + @Override + public Mono transform(Object result, Event event) { + if (result instanceof String s && event instanceof ChatInputInteractionEvent e) { + return e.reply(s); + } + throw new IllegalArgumentException(); + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/interactions/EventResponse.java b/library/src/main/java/net/stelitop/mad4j/interactions/EventResponse.java new file mode 100644 index 0000000..626bcda --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/interactions/EventResponse.java @@ -0,0 +1,130 @@ +package net.stelitop.mad4j.interactions; + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.event.domain.interaction.ComponentInteractionEvent; +import discord4j.core.event.domain.interaction.InteractionCreateEvent; +import discord4j.core.object.component.LayoutComponent; +import discord4j.core.spec.EmbedCreateSpec; +import discord4j.core.spec.InteractionApplicationCommandCallbackReplyMono; +import net.stelitop.mad4j.commands.components.ComponentInteraction; +import reactor.core.publisher.Mono; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class EventResponse { + + private enum ResponseContent { + PLAINTEXT, + EMBED, + UI + } + private enum ResponseAction { + CREATE, + EDIT, + REPLY + } + + private ResponseAction actionType; + private ResponseContent contentType; + + private String plaintextContent; + private EmbedCreateSpec embedContent; + // private UI uiContent; + + private boolean ephemereal = false; + private LayoutComponent[] components = new LayoutComponent[0]; + + private EventResponse(ResponseAction actionType, ResponseContent contentType) { + this.actionType = actionType; + this.contentType = contentType; + } + + public static EventResponse createPlaintext(String message) { + EventResponse ret = new EventResponse(ResponseAction.CREATE, ResponseContent.PLAINTEXT); + ret.plaintextContent = message; + return ret; + } + public static EventResponse replyPlaintext(String message) { + EventResponse ret = new EventResponse(ResponseAction.REPLY, ResponseContent.PLAINTEXT); + ret.plaintextContent = message; + return ret; + } + public static EventResponse editPlaintext(String message) { + EventResponse ret = new EventResponse(ResponseAction.EDIT, ResponseContent.PLAINTEXT); + ret.plaintextContent = message; + return ret; + } + public static EventResponse createEmbed(EmbedCreateSpec embed) { + EventResponse ret = new EventResponse(ResponseAction.CREATE, ResponseContent.EMBED); + ret.embedContent = embed; + return ret; + } + public static EventResponse replyEmbed(EmbedCreateSpec embed) { + EventResponse ret = new EventResponse(ResponseAction.REPLY, ResponseContent.EMBED); + ret.embedContent = embed; + return ret; + } + public static EventResponse editEmbed(EmbedCreateSpec embed) { + EventResponse ret = new EventResponse(ResponseAction.EDIT, ResponseContent.EMBED); + ret.embedContent = embed; + return ret; + } + public static EventResponse createUI(/*Custom UI Object*/) { + throw new UnsupportedOperationException(); + } + public static EventResponse replyUI(/*Custom UI Object*/) { + throw new UnsupportedOperationException(); + } + public static EventResponse editUI(/*Custom UI Object*/) { + throw new UnsupportedOperationException(); + } + + public EventResponse ephemeral() { + this.ephemereal = true; + return this; + } + public EventResponse ephemeral(boolean value) { + this.ephemereal = value; + return this; + } + public EventResponse components(LayoutComponent... components) { + this.components = components; + return this; + } + + public Mono respond(InteractionCreateEvent event) { + if (event instanceof ChatInputInteractionEvent ciie) return respondToSlashCommand(ciie); + else if (event instanceof ComponentInteractionEvent cie) return respondToComponentInteraction(cie); + throw new UnsupportedOperationException("This type of responses is not yet handled!"); + } + + private Mono respondToSlashCommand(ChatInputInteractionEvent event) { + if (actionType != ResponseAction.CREATE && actionType != ResponseAction.REPLY) { + throw new IllegalStateException("You can only create or reply to a command!"); + } + + InteractionApplicationCommandCallbackReplyMono replyMono = switch (contentType) { + case PLAINTEXT -> event.reply(plaintextContent); + case EMBED -> event.reply().withEmbeds(embedContent); + default -> throw new UnsupportedOperationException("This type of responses is not yet handled!"); + }; + return replyMono.withEphemeral(ephemereal).withComponents(components); + } + + private Mono respondToComponentInteraction(ComponentInteractionEvent event) { + return (switch (actionType) { + case CREATE, REPLY -> switch (contentType) { + case PLAINTEXT -> event.reply(plaintextContent); + case EMBED -> event.reply().withEmbeds(embedContent); + default -> throw new UnsupportedOperationException("This type of responses is not yet handled!"); + }; + case EDIT -> switch (contentType) { + case PLAINTEXT -> event.edit(plaintextContent); + case EMBED -> event.edit().withEmbeds(embedContent); + default -> throw new UnsupportedOperationException("This type of responses is not yet handled!"); + }; + }); + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/listeners/CommandOptionAutocompleteListener.java b/library/src/main/java/net/stelitop/mad4j/listeners/CommandOptionAutocompleteListener.java index aa36a07..344fc88 100644 --- a/library/src/main/java/net/stelitop/mad4j/listeners/CommandOptionAutocompleteListener.java +++ b/library/src/main/java/net/stelitop/mad4j/listeners/CommandOptionAutocompleteListener.java @@ -5,8 +5,8 @@ import discord4j.core.object.command.ApplicationCommandInteractionOption; import discord4j.discordjson.json.ApplicationCommandOptionChoiceData; import net.stelitop.mad4j.utils.OptionType; -import net.stelitop.mad4j.autocomplete.AutocompletionExecutor; -import net.stelitop.mad4j.autocomplete.InputSuggestion; +import net.stelitop.mad4j.commands.autocomplete.AutocompletionExecutor; +import net.stelitop.mad4j.commands.autocomplete.InputSuggestion; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +43,7 @@ public CommandOptionAutocompleteListener( } @Override - public void run(ApplicationArguments args) throws Exception { + public void run(ApplicationArguments args) { client.on(ChatInputAutoCompleteEvent.class, this::handle).subscribe(); } diff --git a/library/src/main/java/net/stelitop/mad4j/listeners/ComponentEventListener.java b/library/src/main/java/net/stelitop/mad4j/listeners/ComponentEventListener.java index cd8cae1..721e5f8 100644 --- a/library/src/main/java/net/stelitop/mad4j/listeners/ComponentEventListener.java +++ b/library/src/main/java/net/stelitop/mad4j/listeners/ComponentEventListener.java @@ -8,12 +8,13 @@ import discord4j.core.object.entity.User; import lombok.Builder; import lombok.ToString; +import net.stelitop.mad4j.interactions.EventResponse; import net.stelitop.mad4j.utils.ActionResult; -import net.stelitop.mad4j.components.ComponentInteraction; +import net.stelitop.mad4j.commands.components.ComponentInteraction; import net.stelitop.mad4j.DiscordEventsComponent; -import net.stelitop.mad4j.InteractionEvent; -import net.stelitop.mad4j.convenience.EventUser; -import net.stelitop.mad4j.convenience.EventUserId; +import net.stelitop.mad4j.commands.InteractionEvent; +import net.stelitop.mad4j.commands.convenience.EventUser; +import net.stelitop.mad4j.commands.convenience.EventUserId; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -152,25 +153,22 @@ private Mono executeEvent(ComponentInteractionEvent event, ImplementationE args[i] = mapParamAR.getResponse(); } - - if (!imp.method.getReturnType().equals(Mono.class)) { - LOGGER.error(errorStart + "'s result could not be cast to Mono. Check method signature."); - return event.reply("Could not cast result of slash command.") - .withEphemeral(true); - } - try { Object result = imp.method.invoke(imp.bean, args); - if (Mono.class.isAssignableFrom(result.getClass())) { + if (result instanceof EventResponse er) { + return er.respond(event); + } + else if (Mono.class.isAssignableFrom(result.getClass())) { return ((Mono) result).cast(Void.class); } - return event.reply("Could not cast result of slash command.") - .withEphemeral(true); + else throw new ClassCastException(); + } catch (IllegalAccessException | InvocationTargetException e) { LOGGER.error(errorStart + " had a problem during invoking!"); - throw new RuntimeException(e); + return event.reply("An error occurred invoking the button!") + .withEphemeral(true); } catch (ClassCastException e) { - LOGGER.error(errorStart + "'s result could not be cast to Mono. Check method signature."); + LOGGER.error(errorStart + "'s result could not be cast to any acceptable type. Check method signature."); return event.reply("Could not cast result of slash command.") .withEphemeral(true); } diff --git a/library/src/main/java/net/stelitop/mad4j/listeners/MessageListener.java b/library/src/main/java/net/stelitop/mad4j/listeners/MessageListener.java new file mode 100644 index 0000000..f60e467 --- /dev/null +++ b/library/src/main/java/net/stelitop/mad4j/listeners/MessageListener.java @@ -0,0 +1,173 @@ +package net.stelitop.mad4j.listeners; + +import discord4j.common.util.Snowflake; +import discord4j.core.GatewayDiscordClient; +import discord4j.core.event.domain.message.MessageCreateEvent; +import discord4j.core.object.entity.User; +import discord4j.discordjson.Id; +import net.stelitop.mad4j.commands.CommandData; +import net.stelitop.mad4j.commands.CommandParam; +import net.stelitop.mad4j.commands.CommandType; +import net.stelitop.mad4j.commands.convenience.EventUser; +import net.stelitop.mad4j.commands.convenience.EventUserId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +@Component +public class MessageListener implements ApplicationRunner { + + private final Logger LOGGER = LoggerFactory.getLogger(this.getClass()); + + private final ApplicationContext applicationContext; + private final GatewayDiscordClient client; + private final CommandData commandData; + + @Autowired + public MessageListener( + ApplicationContext applicationContext, + GatewayDiscordClient client, + CommandData commandData + ) { + this.applicationContext = applicationContext; + this.client = client; + this.commandData = commandData; + } + + @Override + public void run(ApplicationArguments args) { + client.on(MessageCreateEvent.class, this::handle).subscribe(); + } + + private Mono handle(MessageCreateEvent event) { + String content = event.getMessage().getContent(); + // TODO: Replace with proper prefix check that is based on Guild ID or a default one in DMs + if (!content.startsWith("!")) return Mono.empty(); + + String contentNoPrefix = content.substring("!".length()); + List parts = splitMessage(contentNoPrefix); + if (parts.size() == 0) return Mono.empty(); + String commandName = parts.get(0); + int commandCutoff = 1; + CommandData.Entry command = commandData.get(commandName, CommandType.Text); + while (commandCutoff < parts.size() && command == null) { + commandName += " " + parts.get(commandCutoff); + commandCutoff++; + command = commandData.get(commandName, CommandType.Text); + } + if (command == null) return Mono.empty(); + +// List commandParams = parts +// .subList(commandCutoff, parts.size()) +// .stream() +// .map(s -> parseParam(s, event)) +// .toList(); + + List methodParams = getOrderedMethodParams(event, parts, command.getMethod()); + try { + command.getMethod().invoke(command.getBean(), methodParams); + } catch (IllegalAccessException | InvocationTargetException e) { + //throw new RuntimeException(e); + return Mono.empty(); + } + + return Mono.empty(); + } + + /** + * Splits a command message into separate parts separated by spaces. Parts surrounded + * by quotation marks are counted as a single string. + * + * @param rawContent The message received by the bot, with the command prefix excluded + * @return List of parts from the emssage. + */ + public List splitMessage(String rawContent) { + String[] parts = rawContent.split(" "); + List ret = new ArrayList<>(); + for (int i = 0; i < parts.length; i++) { + if (parts[i].isBlank()) continue; + if (parts[i].startsWith("\"")) { + List segments = new ArrayList<>(); + while (i < parts.length) { + segments.add(parts[i]); + if (parts[i].endsWith("\"") && !parts[i].endsWith("\\\"")) break; + i++; + } + String part = String.join(" ", segments); + System.out.println(part); + if (part.endsWith("\"") && !part.endsWith("\\\"")) part = part.substring(1, part.length() - 1); + ret.add(part); + } + else { + ret.add(parts[i]); + } + } + + return ret; + } + + /** + * Parses a parameter from how it was input as a String into a java object + * @param s + * @return + */ + private Object parseParam(String s, MessageCreateEvent event) { + Optional l; try {l = Optional.of(Long.parseLong(s));} catch (NumberFormatException e) {l = Optional.empty();} + if (l.isPresent()) return l.get(); + Optional d; try {d = Optional.of(Double.parseDouble(s));} catch (NumberFormatException e) {d = Optional.empty();} + if (d.isPresent()) return d.get(); + // TODO: Fix this, manual check + Optional b = Optional.empty(); + if (s.equalsIgnoreCase("true")) b = Optional.of(true); + if (s.equalsIgnoreCase("false")) b = Optional.of(false); + if (b.isPresent()) return b.get(); + + if (s.endsWith(">") && s.length() > 3) { + // TODO: Verify that it is used in a guild + if (s.startsWith("<@&")) { + if (event.getGuildId().isPresent()) { + return client.getRoleById(event.getGuildId().get(), Snowflake.of(Id.of(s.substring(3, s.length() - 1)))).block(); + } + } + else if (s.startsWith("<@")) return client.getUserById(Snowflake.of(Id.of(s.substring(2, s.length() - 1)))).block(); + else if (s.startsWith("<#")) return client.getChannelById(Snowflake.of(Id.of(s.substring(2, s.length() - 1)))).block(); + } + + return s; + } + + /** + * Creates the parameters + * @param event + * @param commandParams + * @return + */ + private List getOrderedMethodParams(MessageCreateEvent event, List commandParams, Method method) { + int realParamCount = (int)Arrays.stream(method.getParameters()).filter(x -> x.isAnnotationPresent(CommandParam.class)).count(); + int curRealParam = 0; + List paramsRet = new ArrayList<>(); + for (Parameter param : method.getParameters()) { + if (param.isAnnotationPresent(CommandParam.class)) { + curRealParam++; + } else if (param.isAnnotationPresent(EventUser.class)) { + paramsRet.add(client.getUserById(Snowflake.of(event.getMessage().getUserData().id())).block()); + } else if (param.isAnnotationPresent(EventUserId.class)) { + paramsRet.add(event.getMessage().getUserData().id().asLong()); + } + } + return paramsRet; + } +} diff --git a/library/src/main/java/net/stelitop/mad4j/listeners/SlashCommandListener.java b/library/src/main/java/net/stelitop/mad4j/listeners/SlashCommandListener.java index 6266b8d..ad3fdc0 100644 --- a/library/src/main/java/net/stelitop/mad4j/listeners/SlashCommandListener.java +++ b/library/src/main/java/net/stelitop/mad4j/listeners/SlashCommandListener.java @@ -6,18 +6,16 @@ import discord4j.core.object.command.ApplicationCommandInteractionOptionValue; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; -import lombok.ToString; -import net.stelitop.mad4j.commands.DefaultValue; -import net.stelitop.mad4j.requirements.CommandRequirementExecutor; +import net.stelitop.mad4j.commands.*; +import net.stelitop.mad4j.commands.requirements.CommandRequirementExecutor; +import net.stelitop.mad4j.events.AllowedEventResultHandler; +import net.stelitop.mad4j.interactions.EventResponse; import net.stelitop.mad4j.utils.ActionResult; import net.stelitop.mad4j.DiscordEventsComponent; -import net.stelitop.mad4j.commands.CommandParam; -import net.stelitop.mad4j.InteractionEvent; -import net.stelitop.mad4j.commands.SlashCommand; -import net.stelitop.mad4j.convenience.EventUser; -import net.stelitop.mad4j.convenience.EventUserId; +import net.stelitop.mad4j.commands.convenience.EventUser; +import net.stelitop.mad4j.commands.convenience.EventUserId; import net.stelitop.mad4j.utils.OptionType; -import net.stelitop.mad4j.requirements.CommandRequirement; +import net.stelitop.mad4j.commands.requirements.CommandRequirement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -30,7 +28,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; -import java.lang.reflect.Type; import java.util.*; import java.util.stream.Collectors; @@ -55,18 +52,23 @@ public class SlashCommandListener implements ApplicationRunner { // dependencies private final ApplicationContext applicationContext; private final GatewayDiscordClient client; + private final CommandData commandData; + private final AllowedEventResultHandler allowedEventResultHandler; private final Map, CommandRequirementExecutor> possibleRequirements; - private List slashCommands; @Autowired public SlashCommandListener( ApplicationContext applicationContext, GatewayDiscordClient client, + CommandData commandData, + AllowedEventResultHandler allowedEventResultHandler, List possibleRequirements ) { this.applicationContext = applicationContext; this.client = client; + this.commandData = commandData; + this.allowedEventResultHandler = allowedEventResultHandler; this.possibleRequirements = possibleRequirements.stream() .collect(Collectors.toMap(CommandRequirementExecutor::getClass, x -> x)); } @@ -81,38 +83,12 @@ public SlashCommandListener( @Override public void run(ApplicationArguments args) { - Collection commandBeans = applicationContext.getBeansWithAnnotation(DiscordEventsComponent.class).values(); - - slashCommands = new ArrayList<>(); - for (var bean : commandBeans) { - var slashCommandMethods = Arrays.stream(bean.getClass().getMethods()) - .filter(x -> x.isAnnotationPresent(SlashCommand.class)) - .toList(); - - for (var method : slashCommandMethods) { - var newEntry = new SlashCommandEntry(); - SlashCommand slashCommandAnnotation = method.getAnnotation(SlashCommand.class); - newEntry.method = method; - newEntry.name = slashCommandAnnotation.name().toLowerCase(); - newEntry.sourceBean = bean; - slashCommands.add(newEntry); - } - } - + Collection commandBeans = applicationContext + .getBeansWithAnnotation(DiscordEventsComponent.class) + .values(); client.on(ChatInputInteractionEvent.class, this::handle).subscribe(); } - /** - * Data class for the basic data required to invoke a slash command and analyse it. - */ - @NoArgsConstructor - @AllArgsConstructor - private static class SlashCommandEntry { - private Object sourceBean; - private Method method; - private String name; - } - /** *

Handles the {@link ChatInputInteractionEvent} event.

* @@ -124,7 +100,7 @@ private static class SlashCommandEntry { * @param event The event that occurs. * @return The mono emitted from the event. */ - private Mono handle(ChatInputInteractionEvent event) { + public Mono handle(ChatInputInteractionEvent event) { StringBuilder commandNameBuilder = new StringBuilder(event.getCommandName().toLowerCase()); List options = event.getOptions(); while (options.size() == 1 @@ -139,44 +115,58 @@ private Mono handle(ChatInputInteractionEvent event) { Map optionsMap = options.stream() .collect(Collectors.toMap(x -> x.getName().toLowerCase(), x -> x)); - Optional command = this.slashCommands.stream() - .filter(x -> x.name.equalsIgnoreCase(commandName)) - .findFirst(); + CommandData.Entry command = commandData.get(commandName, CommandType.Slash); - if (command.isEmpty()) { + if (command == null) { return event.reply("Could not resolve command '" + commandName + "'.") .withEphemeral(true); } - return invokeSlashCommand(event, optionsMap, command.get()); + return invokeSlashCommand(event, optionsMap, command); } private Mono invokeSlashCommand( ChatInputInteractionEvent event, Map options, - SlashCommandEntry command + CommandData.Entry command ) { - ActionResult conditionsResult = verifyCommandConditions(event, command); + ActionResult conditionsResult = verifyCommandRequirements(event, command); if (conditionsResult.hasFailed()) { return event.reply(conditionsResult.errorMessage()) .withEphemeral(true); } - Parameter[] parameters = command.method.getParameters(); + Parameter[] parameters = command.getMethod().getParameters(); List invocationParams = Arrays.stream(parameters) .map(p -> mapCommandParamToMethodParam(event, options, p)) .toList(); + try { - Object result = command.method.invoke(command.sourceBean, invocationParams.toArray()); - return (Mono) result; + Object result = command.getMethod().invoke(command.getBean(), invocationParams.toArray()); + var eventResponse = allowedEventResultHandler.handleEventResult(result, event); + if (eventResponse.isSuccessful()) { + return eventResponse.getResponse(); + } else { + // TODO: Handle with an exception + return event.reply("An error occurred invoking this slash command!") + .withEphemeral(true); + } +// if (result instanceof EventResponse er) { +// return er.respond(event); +// } +// else if (Mono.class.isAssignableFrom(result.getClass())) { +// return ((Mono) result).cast(Void.class); +// } +// else throw new ClassCastException(); + } catch (IllegalAccessException | InvocationTargetException e) { - LOGGER.error(command.name + " had a problem during invoking."); + LOGGER.error(command.getName() + " had a problem during invoking."); e.printStackTrace(); - return event.reply("An error occurred invoking the slash command!") + return event.reply("An error occurred invoking this slash command!") .withEphemeral(true); } catch (ClassCastException e) { - LOGGER.error(command.name + "'s result could not be cast to Mono. Check method signature."); + LOGGER.error(command.getName() + "'s result could not be cast to any acceptable type. Check method signature."); e.printStackTrace(); return event.reply("Could not cast result of slash command.") .withEphemeral(true); @@ -263,18 +253,21 @@ private Object getValueFromOption(ApplicationCommandInteractionOption option, Pa } /** - * Checks - * @param event - * @param command - * @return + * Checks that all attached command requirements have been fulfilled. If any of them are not, + * the first that fails returns their error message. + * + * @param event The even that triggered the slash command. + * @param command The data about the command. + * @return An action result that succeeds if the condition is fulfilled or fails with an error + * message otherwise. */ - private ActionResult verifyCommandConditions(ChatInputInteractionEvent event, SlashCommandEntry command) { - List conditionAnnotations = Arrays.stream(command.method.getAnnotations()) + private ActionResult verifyCommandRequirements(ChatInputInteractionEvent event, CommandData.Entry command) { + List requirementAnnotations = Arrays.stream(command.getMethod().getAnnotations()) .map(a -> a.annotationType().getAnnotation(CommandRequirement.class)) .filter(Objects::nonNull) .toList(); - for (var annotation : conditionAnnotations) { + for (var annotation : requirementAnnotations) { CommandRequirementExecutor conditionBean = possibleRequirements.get(annotation.implementation()); if (conditionBean == null) continue; ActionResult result = conditionBean.verify(event); diff --git a/library/src/test/java/slashcommands/registering/BaseTestConfiguration.java b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/BaseTestConfiguration.java similarity index 69% rename from library/src/test/java/slashcommands/registering/BaseTestConfiguration.java rename to library/src/test/java/test/net/stelitop/mad4j/slashcommands/BaseTestConfiguration.java index 9e78e9a..8408445 100644 --- a/library/src/test/java/slashcommands/registering/BaseTestConfiguration.java +++ b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/BaseTestConfiguration.java @@ -1,5 +1,6 @@ -package slashcommands.registering; +package test.net.stelitop.mad4j.slashcommands; +import discord4j.core.DiscordClient; import discord4j.core.GatewayDiscordClient; import discord4j.rest.RestClient; import discord4j.rest.service.ApplicationService; @@ -21,19 +22,20 @@ public class BaseTestConfiguration { public static long TEST_APPLICATION_ID = 1L; @Bean - public GatewayDiscordClient gatewayDiscordClient(RestClient restClientMock) { + public GatewayDiscordClient gatewayDiscordClient(DiscordClient clientMock) { GatewayDiscordClient gatewayDiscordClientMock = mock(GatewayDiscordClient.class); - when(gatewayDiscordClientMock.getRestClient()).thenReturn(restClientMock); + when(gatewayDiscordClientMock.getRestClient()).thenReturn(clientMock); + when(gatewayDiscordClientMock.rest()).thenReturn(clientMock); when(gatewayDiscordClientMock.on(any(), any())).thenReturn(Flux.empty()); return gatewayDiscordClientMock; } @Bean - public RestClient restClient(ApplicationService applicationServiceMock) { - RestClient restClientMock = mock(RestClient.class); - when(restClientMock.getApplicationId()).thenReturn(Mono.just(TEST_APPLICATION_ID)); - when(restClientMock.getApplicationService()).thenReturn(applicationServiceMock); - return restClientMock; + public DiscordClient client(ApplicationService applicationServiceMock) { + DiscordClient clientMock = mock(DiscordClient.class); + when(clientMock.getApplicationId()).thenReturn(Mono.just(TEST_APPLICATION_ID)); + when(clientMock.getApplicationService()).thenReturn(applicationServiceMock); + return clientMock; } @Bean diff --git a/library/src/test/java/slashcommands/registering/BaseTestConfigurationTest.java b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/BaseTestConfigurationTest.java similarity index 90% rename from library/src/test/java/slashcommands/registering/BaseTestConfigurationTest.java rename to library/src/test/java/test/net/stelitop/mad4j/slashcommands/BaseTestConfigurationTest.java index 172500b..b4af623 100644 --- a/library/src/test/java/slashcommands/registering/BaseTestConfigurationTest.java +++ b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/BaseTestConfigurationTest.java @@ -1,4 +1,4 @@ -package slashcommands.registering; +package test.net.stelitop.mad4j.slashcommands; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; diff --git a/library/src/test/java/test/net/stelitop/mad4j/slashcommands/executing/PlainCommandTest.java b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/executing/PlainCommandTest.java new file mode 100644 index 0000000..b720809 --- /dev/null +++ b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/executing/PlainCommandTest.java @@ -0,0 +1,199 @@ +package test.net.stelitop.mad4j.slashcommands.executing; + + +import discord4j.core.GatewayDiscordClient; +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.command.Interaction; +import discord4j.discordjson.Id; +import discord4j.discordjson.json.*; +import discord4j.discordjson.possible.Possible; +import discord4j.rest.service.ApplicationService; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.commands.CommandParam; +import net.stelitop.mad4j.commands.InteractionEvent; +import net.stelitop.mad4j.commands.SlashCommand; +import net.stelitop.mad4j.listeners.SlashCommandListener; +import test.net.stelitop.mad4j.slashcommands.BaseTestConfiguration; +import net.stelitop.mad4j.utils.OptionType; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; +import reactor.core.publisher.Mono; + +import static org.mockito.Mockito.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Import({BaseTestConfiguration.class, PlainCommandTest.TestComponent.class}) +public class PlainCommandTest { + + @Autowired + private GatewayDiscordClient gatewayDiscordClientMock; + @Autowired + private ApplicationService applicationServiceMock; + @Autowired + private SlashCommandListener slashCommandListener; + @DiscordEventsComponent + public static class TestComponent { + + static final String commandDescriptionJoke = "Tells a joke"; + static final String joke = "Why did the chicken cross the road?"; + + @SlashCommand(name = "joke", description = commandDescriptionJoke) + public Mono tellJokeCommand( + @InteractionEvent ChatInputInteractionEvent event + ) { + return event.reply(joke); + } + + static final String commandDescriptionAdd = "Adds two numbers together"; + + @SlashCommand(name = "add", description = commandDescriptionAdd) + public Mono addTwoNumbersCommand( + @InteractionEvent ChatInputInteractionEvent event, + @CommandParam(name = "x", description = "not relevant") long x, + @CommandParam(name = "y", description = "not relevant") long y + ) { + return event.reply(String.valueOf(x + y)); + } + + static final String commandDescriptionOptional = "Command with an optional parameter."; + + @SlashCommand(name = "optional", description = commandDescriptionOptional) + public Mono optionalParamCommand( + @InteractionEvent ChatInputInteractionEvent event, + @CommandParam(name = "string", description = "not relevant", required = false) String s + ) { + if (s == null) { + return event.reply("No value"); + } else { + return event.reply("String = " + s); + } + } + } + + @Test + public void noParamsCommandJoke() { + var event = new ChatInputInteractionEvent(gatewayDiscordClientMock, null, new Interaction(gatewayDiscordClientMock, + ImmutableInteractionData.of( + Id.of("0"), + Id.of("0"), + 1, // Chat application command type + Possible.of(ApplicationCommandInteractionData.builder() + .name("joke") + .build()), + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent(), + "token", + 1, // version + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent() + ))); + var eventSpy = spy(event); + slashCommandListener.handle(eventSpy); + + verify(eventSpy).reply(TestComponent.joke); + verify(eventSpy, times(1)).reply((String) any()); + } + + @Test + public void paramsCommandAdd() { + long x = 5, y = 14; + var event = new ChatInputInteractionEvent(gatewayDiscordClientMock, null, new Interaction(gatewayDiscordClientMock, + ImmutableInteractionData.of( + Id.of("0"), + Id.of("0"), + 1, // Chat application command type + Possible.of(ApplicationCommandInteractionData.builder() + .name("add") + .addOption(ApplicationCommandInteractionOptionData.builder().name("x").value(String.valueOf(x)).type(OptionType.INTEGER).build()) + .addOption(ApplicationCommandInteractionOptionData.builder().name("y").value(String.valueOf(y)).type(OptionType.INTEGER).build()) + .build()), + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent(), + "token", + 1, // version + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent() + ))); + var eventSpy = spy(event); + slashCommandListener.handle(eventSpy); + + verify(eventSpy).reply(String.valueOf(x + y)); + verify(eventSpy, times(1)).reply((String) any()); + } + + @Test + public void optionalParamPresent() { + String inputString = "abcdef12345"; + var event = new ChatInputInteractionEvent(gatewayDiscordClientMock, null, new Interaction(gatewayDiscordClientMock, + ImmutableInteractionData.of( + Id.of("0"), + Id.of("0"), + 1, // Chat application command type + Possible.of(ApplicationCommandInteractionData.builder() + .name("optional") + .addOption(ApplicationCommandInteractionOptionData.builder().name("string").value(inputString).type(OptionType.STRING).build()) + .build()), + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent(), + "token", + 1, // version + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent() + ))); + var eventSpy = spy(event); + slashCommandListener.handle(eventSpy); + + verify(eventSpy).reply(contains(inputString)); + verify(eventSpy, times(1)).reply((String) any()); + } + + @Test + public void optionalParamMissing() { + String inputString = "abcdef12345"; + var event = new ChatInputInteractionEvent(gatewayDiscordClientMock, null, new Interaction(gatewayDiscordClientMock, + ImmutableInteractionData.of( + Id.of("0"), + Id.of("0"), + 1, // Chat application command type + Possible.of(ApplicationCommandInteractionData.builder() + .name("optional") + .build()), + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent(), + "token", + 1, // version + Possible.absent(), + Possible.absent(), + Possible.absent(), + Possible.absent() + ))); + var eventSpy = spy(event); + slashCommandListener.handle(eventSpy); + + verify(eventSpy).reply("No value"); + verify(eventSpy, times(1)).reply((String) any()); + } +} diff --git a/library/src/test/java/slashcommands/registering/CommandGroupTest.java b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/CommandGroupTest.java similarity index 94% rename from library/src/test/java/slashcommands/registering/CommandGroupTest.java rename to library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/CommandGroupTest.java index b729d6e..fc3313d 100644 --- a/library/src/test/java/slashcommands/registering/CommandGroupTest.java +++ b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/CommandGroupTest.java @@ -1,13 +1,14 @@ -package slashcommands.registering; +package test.net.stelitop.mad4j.slashcommands.registering; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; import discord4j.discordjson.json.ApplicationCommandOptionData; import discord4j.discordjson.json.ApplicationCommandRequest; import discord4j.rest.service.ApplicationService; import net.stelitop.mad4j.DiscordEventsComponent; -import net.stelitop.mad4j.InteractionEvent; +import net.stelitop.mad4j.commands.InteractionEvent; import net.stelitop.mad4j.commands.CommandParam; import net.stelitop.mad4j.commands.SlashCommand; +import test.net.stelitop.mad4j.slashcommands.BaseTestConfiguration; import net.stelitop.mad4j.utils.OptionType; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; diff --git a/library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/IncorrectCommandMethodDefinitionTest.java b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/IncorrectCommandMethodDefinitionTest.java new file mode 100644 index 0000000..f8bb11b --- /dev/null +++ b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/IncorrectCommandMethodDefinitionTest.java @@ -0,0 +1,178 @@ +package test.net.stelitop.mad4j.slashcommands.registering; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.commands.*; +import net.stelitop.mad4j.commands.convenience.EventUser; +import net.stelitop.mad4j.commands.convenience.EventUserId; +import org.slf4j.LoggerFactory; +import test.net.stelitop.mad4j.slashcommands.BaseTestConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Import; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class IncorrectCommandMethodDefinitionTest { + + public static class LongNameTest { + private static final String commandName = "name with too many parts"; + @DiscordEventsComponent + public static class TestComponent { + + @SlashCommand(name = commandName, description = "Not relevant") + public Mono longNameCommand( + @InteractionEvent ChatInputInteractionEvent event + ) { + return event.reply("Reply!"); + } + } + + @EnableAutoConfiguration + @Import({BaseTestConfiguration.class, TestComponent.class}) + protected static class TestApplication { + + } + + @Test + public void test() { +// Logger registrarLogger = (Logger) LoggerFactory.getLogger(SlashCommandRegistrar.class); +// ListAppender listAppender = new ListAppender<>(); +// listAppender.start(); +// registrarLogger.addAppender(listAppender); + + SpringApplication springApplication = new SpringApplication(TestApplication.class); + assertThrows(RuntimeException.class, springApplication::run); + +// System.out.println(listAppender.list.size()); + +// assertTrue(listAppender.list.stream() +// .anyMatch(x -> x.getLevel().equals(Level.ERROR) && x.getFormattedMessage().contains(commandName))); + } + } + + public static class InvalidParamTypeTest { + @DiscordEventsComponent + public static class TestComponent { + + public static class CustomType { + + } + @SlashCommand(name = "notrelevant", description = "Not relevant") + public Mono longNameCommand( + @InteractionEvent ChatInputInteractionEvent event, + @CommandParam(name = "param", description = "Not relevant") CustomType par + ) { + return event.reply("Reply!"); + } + } + + @EnableAutoConfiguration + @Import({BaseTestConfiguration.class, TestComponent.class}) + protected static class TestApplication { + + } + + @Test + public void test() { + SpringApplication springApplication = new SpringApplication(TestApplication.class); + assertThrows(RuntimeException.class, springApplication::run); + } + } + + public static class PrimitiveParamWithDefaultValueTest { + @DiscordEventsComponent + public static class TestComponent { + + public static class CustomType { + + } + @SlashCommand(name = "notrelevant", description = "Not relevant") + public Mono longNameCommand( + @InteractionEvent ChatInputInteractionEvent event, + @CommandParam(name = "param", description = "Not relevant", required = false) + @DefaultValue(number = 123) + long par + ) { + return event.reply("Reply!"); + } + } + + @EnableAutoConfiguration + @Import({BaseTestConfiguration.class, TestComponent.class}) + protected static class TestApplication { + + } + + @Test + public void test() { + SpringApplication springApplication = new SpringApplication(TestApplication.class); + assertThrows(RuntimeException.class, springApplication::run); + } + } + + public static class EventUserInjectionWrongTypeTest { + @DiscordEventsComponent + public static class TestComponent { + + public static class CustomType { + + } + @SlashCommand(name = "notrelevant", description = "Not relevant") + public Mono longNameCommand( + @InteractionEvent ChatInputInteractionEvent event, + @EventUser long user + ) { + return event.reply("Reply!"); + } + } + + @EnableAutoConfiguration + @Import({BaseTestConfiguration.class, TestComponent.class}) + protected static class TestApplication { + + } + + @Test + public void test() { + SpringApplication springApplication = new SpringApplication(TestApplication.class); + assertThrows(RuntimeException.class, springApplication::run); + } + } + + public static class EventUserIdInjectionWrongTypeTest { + @DiscordEventsComponent + public static class TestComponent { + + public static class CustomType { + + } + @SlashCommand(name = "notrelevant", description = "Not relevant") + public Mono longNameCommand( + @InteractionEvent ChatInputInteractionEvent event, + @EventUserId double userId + ) { + return event.reply("Reply!"); + } + } + + @EnableAutoConfiguration + @Import({BaseTestConfiguration.class, TestComponent.class}) + protected static class TestApplication { + + } + + @Test + public void test() { + SpringApplication springApplication = new SpringApplication(TestApplication.class); + assertThrows(RuntimeException.class, springApplication::run); + } + } +} diff --git a/library/src/test/java/slashcommands/registering/PlainCommandTest.java b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/PlainCommandTest.java similarity index 94% rename from library/src/test/java/slashcommands/registering/PlainCommandTest.java rename to library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/PlainCommandTest.java index 55dfa69..eae6d34 100644 --- a/library/src/test/java/slashcommands/registering/PlainCommandTest.java +++ b/library/src/test/java/test/net/stelitop/mad4j/slashcommands/registering/PlainCommandTest.java @@ -1,15 +1,14 @@ -package slashcommands.registering; +package test.net.stelitop.mad4j.slashcommands.registering; -import discord4j.core.GatewayDiscordClient; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; import discord4j.discordjson.json.ApplicationCommandOptionData; import discord4j.discordjson.json.ApplicationCommandRequest; -import discord4j.rest.RestClient; import discord4j.rest.service.ApplicationService; import net.stelitop.mad4j.DiscordEventsComponent; -import net.stelitop.mad4j.InteractionEvent; +import net.stelitop.mad4j.commands.InteractionEvent; import net.stelitop.mad4j.commands.CommandParam; import net.stelitop.mad4j.commands.SlashCommand; +import test.net.stelitop.mad4j.slashcommands.BaseTestConfiguration; import net.stelitop.mad4j.utils.OptionType; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; diff --git a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commandrequirements/UnusableCommand.java b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commandrequirements/UnusableCommand.java index 434cb9a..f548800 100644 --- a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commandrequirements/UnusableCommand.java +++ b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commandrequirements/UnusableCommand.java @@ -1,6 +1,6 @@ package net.stelitop.generalbot.commandrequirements; -import net.stelitop.mad4j.requirements.CommandRequirement; +import net.stelitop.mad4j.commands.requirements.CommandRequirement; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commandrequirements/UnusableCommandImplementation.java b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commandrequirements/UnusableCommandImplementation.java index def391b..f8d8322 100644 --- a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commandrequirements/UnusableCommandImplementation.java +++ b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commandrequirements/UnusableCommandImplementation.java @@ -1,7 +1,7 @@ package net.stelitop.generalbot.commandrequirements; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; -import net.stelitop.mad4j.requirements.CommandRequirementExecutor; +import net.stelitop.mad4j.commands.requirements.CommandRequirementExecutor; import net.stelitop.mad4j.utils.ActionResult; import org.springframework.stereotype.Component; diff --git a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slashcommands/BasicCommands.java b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/BasicCommands.java similarity index 61% rename from test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slashcommands/BasicCommands.java rename to test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/BasicCommands.java index c8211b8..725b59a 100644 --- a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slashcommands/BasicCommands.java +++ b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/BasicCommands.java @@ -1,12 +1,17 @@ -package net.stelitop.generalbot.commands.slashcommands; +package net.stelitop.generalbot.commands.slash; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.entity.User; +import discord4j.core.spec.EmbedCreateSpec; +import discord4j.rest.util.Color; import net.stelitop.generalbot.commandrequirements.UnusableCommand; import net.stelitop.mad4j.DiscordEventsComponent; -import net.stelitop.mad4j.InteractionEvent; +import net.stelitop.mad4j.commands.InteractionEvent; import net.stelitop.mad4j.commands.CommandParam; import net.stelitop.mad4j.commands.DefaultValue; import net.stelitop.mad4j.commands.SlashCommand; +import net.stelitop.mad4j.commands.requirements.standard.DMCommandRequirement; +import net.stelitop.mad4j.commands.requirements.standard.GuildCommandRequirement; import org.springframework.beans.factory.annotation.Autowired; import reactor.core.publisher.Mono; @@ -85,4 +90,55 @@ public Mono unusableCommand( ) { return event.reply("Used! This is an error, the requirement is not working."); } + + @SlashCommand( + name = "basic userchannel", + description = "Tests the injection of user and channel parameters." + ) + public Mono userchanneLCommand( + @InteractionEvent + ChatInputInteractionEvent event, + @CommandParam(name = "user", description = "User") + User user + ) { + return event.reply("User = " + user.getTag()); + } + + @SlashCommand( + name = "basic stringresponse", + description = "Responses by returning a string." + ) + public String stringResponse() { + return "This was returned as a string!"; + } + + @SlashCommand( + name = "basic embedresponse", + description = "Responses by returning a string." + ) + public EmbedCreateSpec embedResponse() { + return EmbedCreateSpec.builder() + .title("Embed Title") + .description("Embed Description") + .color(Color.BLUE) + .build(); + } + + @GuildCommandRequirement + @SlashCommand( + name = "basic guildonly", + description = "Only usable in guilds." + ) + public String guildOnlyCommand() { + return "Hello guild!"; + } + + @DMCommandRequirement + @SlashCommand( + name = "basic dmonly", + description = "Only usable in guilds." + ) + public String dmOnlyCommand() { + return "Hello dms!"; + } } diff --git a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slashcommands/ComponentCommands.java b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/ComponentCommands.java similarity index 92% rename from test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slashcommands/ComponentCommands.java rename to test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/ComponentCommands.java index 390c355..e202691 100644 --- a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slashcommands/ComponentCommands.java +++ b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/ComponentCommands.java @@ -1,4 +1,4 @@ -package net.stelitop.generalbot.commands.slashcommands; +package net.stelitop.generalbot.commands.slash; import discord4j.core.event.domain.interaction.ButtonInteractionEvent; import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; @@ -7,9 +7,9 @@ import discord4j.core.object.component.Button; import discord4j.core.object.component.SelectMenu; import net.stelitop.mad4j.DiscordEventsComponent; -import net.stelitop.mad4j.InteractionEvent; +import net.stelitop.mad4j.commands.InteractionEvent; import net.stelitop.mad4j.commands.SlashCommand; -import net.stelitop.mad4j.components.ComponentInteraction; +import net.stelitop.mad4j.commands.components.ComponentInteraction; import reactor.core.publisher.Mono; import java.util.List; diff --git a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/EventResponseCommands.java b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/EventResponseCommands.java new file mode 100644 index 0000000..d21840d --- /dev/null +++ b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/slash/EventResponseCommands.java @@ -0,0 +1,77 @@ +package net.stelitop.generalbot.commands.slash; + + +import discord4j.core.event.domain.interaction.ButtonInteractionEvent; +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.core.object.component.ActionRow; +import discord4j.core.object.component.Button; +import discord4j.core.spec.EmbedCreateSpec; +import discord4j.rest.util.Color; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.commands.InteractionEvent; +import net.stelitop.mad4j.commands.SlashCommand; +import net.stelitop.mad4j.commands.components.ComponentInteraction; +import net.stelitop.mad4j.interactions.EventResponse; +import reactor.core.publisher.Mono; + +import java.util.Random; + +@DiscordEventsComponent +public class EventResponseCommands { + + @SlashCommand( + name = "eventresponse normal plaintext", + description = "Uses EventResponse instead of directly replying to the event. Returns plaintext." + ) + public EventResponse normalPlaintext() { + return EventResponse.createPlaintext("This was created using an event response!"); + } + + @SlashCommand( + name = "eventresponse normal embed", + description = "Uses EventResponse instead of directly replying to the event. Returns an embed." + ) + public EventResponse normalEmbed() { + EmbedCreateSpec embed = EmbedCreateSpec.builder() + .color(Color.TAHITI_GOLD) + .title("Fancy embed!") + .description("Embed description.") + .build(); + return EventResponse.createEmbed(embed); + } + + @ComponentInteraction( + regex = "ButtonEventResponseEditPlainText", + event = ButtonInteractionEvent.class + ) + public EventResponse eventResponseButtonEditPlaintext() { + return EventResponse.editPlaintext("Edited text! Random number: " + new Random().nextInt(10000)); + } + + @SlashCommand( + name = "eventresponse button editplaintext", + description = "Uses EventResponse instead of directly replying to the event. Returns an embed." + ) + public EventResponse buttonWithResponseEditPlaintext() { + return EventResponse.createPlaintext("Click the button for it to reply with an event response.") + .components(ActionRow.of(Button.primary("ButtonEventResponseEditPlainText", "Event response button - edit plaintext."))); + } + + @ComponentInteraction( + regex = "ButtonEventResponseCreatePlainText", + event = ButtonInteractionEvent.class + ) + public EventResponse eventResponseButtonCreatePlaintext() { + return EventResponse.createPlaintext("Created new text!"); + } + + @SlashCommand( + name = "eventresponse button createplaintext", + description = "Uses EventResponse instead of directly replying to the event. Returns an embed." + ) + public Mono buttonWithResponseCreatePlaintext(@InteractionEvent ChatInputInteractionEvent event) { + + return event.reply("Click the button for it to reply with an event response") + .withComponents(ActionRow.of(Button.primary("ButtonEventResponseCreatePlainText", "Event response button - create plaintext."))); + } +} diff --git a/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/text/BasicTextCommand.java b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/text/BasicTextCommand.java new file mode 100644 index 0000000..6a6125f --- /dev/null +++ b/test-bots/general-bot/src/main/java/net/stelitop/generalbot/commands/text/BasicTextCommand.java @@ -0,0 +1,19 @@ +package net.stelitop.generalbot.commands.text; + +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.commands.Command; +import net.stelitop.mad4j.commands.CommandType; +import net.stelitop.mad4j.interactions.EventResponse; + +@DiscordEventsComponent +public class BasicTextCommand { + + @Command( + name = "textping", + description = "Replies back with pong.", + types = {CommandType.Text} + ) + public EventResponse pingCommand() { + return EventResponse.createPlaintext("pong"); + } +} diff --git a/test-bots/general-bot/src/main/resources/application.properties b/test-bots/general-bot/src/main/resources/application.properties index f8135a1..0964ff8 100644 --- a/test-bots/general-bot/src/main/resources/application.properties +++ b/test-bots/general-bot/src/main/resources/application.properties @@ -1,2 +1,2 @@ spring.application.name=generalbot -mad4j.slashcommands.update=true \ No newline at end of file +mad4j.slashcommands.update=false \ No newline at end of file diff --git a/test-bots/general-bot/src/test/java/net/stelitop/generalbot/GeneralbotApplicationTests.java b/test-bots/general-bot/src/test/java/net/stelitop/generalbot/GeneralbotApplicationTests.java index ef1a6a1..95f093b 100644 --- a/test-bots/general-bot/src/test/java/net/stelitop/generalbot/GeneralbotApplicationTests.java +++ b/test-bots/general-bot/src/test/java/net/stelitop/generalbot/GeneralbotApplicationTests.java @@ -3,11 +3,14 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +/** + * The test fails the GitHub pipeline because it does not see the testbotconfig.json file. + */ @SpringBootTest class GeneralbotApplicationTests { - @Test - void contextLoads() { - } +// @Test +// void contextLoads() { +// } }