diff --git a/pom.xml b/pom.xml index f45bedc44..d8298bc0d 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,23 @@ https://javadoc.io/doc/net.dv8tion/JDA/5.0.2 true + + + apiNote + a + API Note: + + + implSpec + a + Implementation Requirements: + + + implNote + a + Implementation Note: + + diff --git a/src/main/java/com/github/kaktushose/jda/commands/JDACommands.java b/src/main/java/com/github/kaktushose/jda/commands/JDACommands.java index 2e1346a1d..cbeb02375 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/JDACommands.java +++ b/src/main/java/com/github/kaktushose/jda/commands/JDACommands.java @@ -1,13 +1,20 @@ package com.github.kaktushose.jda.commands; +import com.github.kaktushose.jda.commands.annotations.interactions.CommandScope; import com.github.kaktushose.jda.commands.annotations.interactions.EntitySelectMenu; import com.github.kaktushose.jda.commands.annotations.interactions.StringSelectMenu; +import com.github.kaktushose.jda.commands.definitions.description.reflective.ReflectiveClassFinder; +import com.github.kaktushose.jda.commands.definitions.description.reflective.ReflectiveDescriptor; +import com.github.kaktushose.jda.commands.definitions.interactions.CustomId; +import com.github.kaktushose.jda.commands.definitions.interactions.InteractionRegistry; +import com.github.kaktushose.jda.commands.definitions.interactions.component.ButtonDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.SelectMenuDefinition; import com.github.kaktushose.jda.commands.dependency.DefaultDependencyInjector; import com.github.kaktushose.jda.commands.dependency.DependencyInjector; -import com.github.kaktushose.jda.commands.dispatching.ExpirationStrategy; -import com.github.kaktushose.jda.commands.dispatching.internal.JDAEventListener; -import com.github.kaktushose.jda.commands.dispatching.internal.Runtime; +import com.github.kaktushose.jda.commands.dispatching.ImplementationRegistry; +import com.github.kaktushose.jda.commands.dispatching.JDAEventListener; import com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry; +import com.github.kaktushose.jda.commands.dispatching.expiration.ExpirationStrategy; import com.github.kaktushose.jda.commands.dispatching.handling.DispatchingContext; import com.github.kaktushose.jda.commands.dispatching.middleware.MiddlewareRegistry; import com.github.kaktushose.jda.commands.dispatching.middleware.Priority; @@ -17,10 +24,6 @@ import com.github.kaktushose.jda.commands.dispatching.validation.ValidatorRegistry; import com.github.kaktushose.jda.commands.internal.JDAContext; import com.github.kaktushose.jda.commands.internal.register.SlashCommandUpdater; -import com.github.kaktushose.jda.commands.reflect.ImplementationRegistry; -import com.github.kaktushose.jda.commands.reflect.InteractionRegistry; -import com.github.kaktushose.jda.commands.reflect.interactions.components.ButtonDefinition; -import com.github.kaktushose.jda.commands.reflect.interactions.components.menus.GenericSelectMenuDefinition; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; import net.dv8tion.jda.api.interactions.commands.localization.ResourceBundleLocalizationFunction; @@ -53,7 +56,7 @@ private static JDACommands startInternal(Object jda, Class clazz, Localizatio var adapterRegistry = new TypeAdapterRegistry(); var validatorRegistry = new ValidatorRegistry(); var implementationRegistry = new ImplementationRegistry(dependencyInjector, middlewareRegistry, adapterRegistry, validatorRegistry); - var interactionRegistry = new InteractionRegistry(validatorRegistry, dependencyInjector, function); + var interactionRegistry = new InteractionRegistry(dependencyInjector, validatorRegistry, function, new ReflectiveDescriptor()); middlewareRegistry.register(Priority.PERMISSIONS, new PermissionsMiddleware(implementationRegistry)); middlewareRegistry.register(Priority.NORMAL, new ConstraintMiddleware(implementationRegistry), new CooldownMiddleware(implementationRegistry)); @@ -62,7 +65,7 @@ private static JDACommands startInternal(Object jda, Class clazz, Localizatio implementationRegistry.index(clazz, packages); - interactionRegistry.index(clazz, packages); + interactionRegistry.index(ReflectiveClassFinder.find(clazz, packages)); var updater = new SlashCommandUpdater(jdaContext, implementationRegistry.getGuildScopeProvider(), interactionRegistry); updater.updateAllCommands(); @@ -171,14 +174,14 @@ public void shutdown() { /** * Updates all slash commands that are registered with - * {@link com.github.kaktushose.jda.commands.annotations.interactions.SlashCommand.CommandScope#GUILD + * {@link CommandScope#GUILD * CommandScope#Guild} */ public void updateGuildCommands() { updater.updateGuildCommands(); } - /// Gets a [`Button`][com.github.kaktushose.jda.commands.annotations.interactions.Button] based on the method name + /// Gets a [`Button`][com.github.kaktushose.jda.commands.annotations.interactions.Button] based on the definition id /// and transforms it into a JDA [Button]. /// /// The button will be [`Runtime`]({@docRoot}/index.html#runtime-concept-heading) independent. This may be useful if you want to send a message without @@ -188,38 +191,22 @@ public void updateGuildCommands() { /// @return the JDA [Button] @NotNull public Button getButton(@NotNull String button) { - if (!button.matches("[a-zA-Z]+\\.[a-zA-Z]+")) { - throw new IllegalArgumentException("Unknown Button"); - } - - String sanitizedId = button.replaceAll("\\.", ""); - ButtonDefinition buttonDefinition = interactionRegistry.getButtons().stream() - .filter(it -> it.getDefinitionId().equals(sanitizedId)) - .findFirst().orElseThrow(() -> new IllegalArgumentException("Unknown Button")); - - return buttonDefinition.toButton().withId(buttonDefinition.independentCustomId()); + var id = String.valueOf(button.replaceAll("\\.", "").hashCode()); + var definition = interactionRegistry.find(ButtonDefinition.class, false, it -> it.definitionId().equals(id)); + return definition.toJDAEntity(CustomId.independent(definition.definitionId())); } - /// Gets a [StringSelectMenu] or [EntitySelectMenu] based on the method name and transforms it into a JDA [SelectMenu]. + /// Gets a [StringSelectMenu] or [EntitySelectMenu] based on the definition id and transforms it into a JDA [SelectMenu]. /// /// The select menu will be [`Runtime`]({@docRoot}/index.html#runtime-concept-heading) independent. This may be useful if you want to send a component /// without using the framework. /// - /// @param the type of [SelectMenu] /// @param menu the name of the select menu /// @return the JDA [SelectMenu] - @SuppressWarnings("unchecked") @NotNull - public S getSelectMenu(@NotNull String menu) { - if (!menu.matches("[a-zA-Z]+\\.[a-zA-Z]+")) { - throw new IllegalArgumentException("Unknown Select Menu"); - } - - String sanitizedId = menu.replaceAll("\\.", ""); - GenericSelectMenuDefinition selectMenuDefinition = interactionRegistry.getSelectMenus().stream() - .filter(it -> it.getDefinitionId().equals(sanitizedId)) - .findFirst().orElseThrow(() -> new IllegalArgumentException("Unknown Select Menu")); - - return (S) selectMenuDefinition.toSelectMenu(selectMenuDefinition.independentCustomId(), true); + public SelectMenu getSelectMenu(@NotNull String menu) { + var id = String.valueOf(menu.replaceAll("\\.", "").hashCode()); + var definition = interactionRegistry.find(SelectMenuDefinition.class, false, it -> it.definitionId().equals(id)); + return (SelectMenu) definition.toJDAEntity(CustomId.independent(definition.definitionId())); } } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/Implementation.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/Implementation.java index 99aedd334..6df27a324 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/Implementation.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/Implementation.java @@ -1,46 +1,40 @@ package com.github.kaktushose.jda.commands.annotations; import com.github.kaktushose.jda.commands.annotations.constraints.Constraint; +import com.github.kaktushose.jda.commands.dispatching.ImplementationRegistry; +import com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapter; +import com.github.kaktushose.jda.commands.dispatching.middleware.Middleware; import com.github.kaktushose.jda.commands.dispatching.middleware.Priority; +import com.github.kaktushose.jda.commands.dispatching.validation.Validator; import com.github.kaktushose.jda.commands.embeds.error.ErrorMessageFactory; +import com.github.kaktushose.jda.commands.permissions.PermissionsProvider; +import com.github.kaktushose.jda.commands.scope.GuildScopeProvider; import java.lang.annotation.*; -/** - * Indicates that the annotated class is a custom implementation that should replace the default implementation. - * - * @see com.github.kaktushose.jda.commands.reflect.ImplementationRegistry ImplementationRegistry - * @see com.github.kaktushose.jda.commands.dispatching.middleware.Middleware Middleware - * @see com.github.kaktushose.jda.commands.dispatching.validation.Validator Validator - * @see com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapter TypeAdapter - * @see com.github.kaktushose.jda.commands.permissions.PermissionsProvider PermissionsProvider - * @see com.github.kaktushose.jda.commands.scope.GuildScopeProvider GuildScopeProvider - * @see ErrorMessageFactory ErrorMessageFactory - * @since 2.0.0 - */ +/// Indicates that the annotated class is a custom implementation that should replace the default implementation. +/// +/// @see ImplementationRegistry +/// @see Middleware +/// @see Validator +/// @see TypeAdapter +/// @see PermissionsProvider +/// @see GuildScopeProvider +/// @see ErrorMessageFactory @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Implementation { - /** - * Gets the {@link Priority} to register the - * {@link com.github.kaktushose.jda.commands.dispatching.middleware.Middleware Middleware} with. If this - * implementation is not a subtype - * of {@link com.github.kaktushose.jda.commands.dispatching.middleware.Middleware Middleware}, this field can be - * ignored. - * - * @return the {@link Priority} - */ + /// Gets the [Priority] to register the [Middleware] with. If this implementation is not a subtype of [Middleware], + /// this field can be ignored. + /// + /// @return the [Priority] Priority priority() default Priority.NORMAL; - /** - * Gets the annotation the {@link com.github.kaktushose.jda.commands.dispatching.validation.Validator Validator} - * should be mapped to. If this class is not a subtype of - * {@link com.github.kaktushose.jda.commands.dispatching.validation.Validator Validator}, this field can be ignored. - * - * @return the annotation the {@link com.github.kaktushose.jda.commands.dispatching.validation.Validator Validator} - * should be mapped to - */ + /// Gets the annotation the [Validator] should be mapped to. If this class is not a subtype of [Validator], + /// this field can be ignored. + /// + /// @return the annotation the [Validator] should be mapped to Class annotation() default Constraint.class; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/Inject.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/Inject.java index 55ccbc257..81371c243 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/Inject.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/Inject.java @@ -5,14 +5,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Fields annotated with Inject will be assigned a value that is provided by a {@link Produces} method. - * If no Producer for the field type is available then the field will be assigned {@code null}. Please note, that each - * field type can only have one producer. - * - * @see Produces - * @since 1.0.0 - */ +/// Fields annotated with Inject will be assigned a value that is provided by a [Produces] method. +/// If no Producer for the field type is available then the field will be assigned `null`. Please note, that each +/// field type can only have one producer. +/// +/// @see Produces @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Inject { diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/Produces.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/Produces.java index 63383d2fc..63aa6c485 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/Produces.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/Produces.java @@ -1,42 +1,37 @@ package com.github.kaktushose.jda.commands.annotations; +import com.github.kaktushose.jda.commands.annotations.interactions.Interaction; +import com.github.kaktushose.jda.commands.dependency.DependencyInjector; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Methods annotated with Produces will be used to get the instance of a dependency. - * - *

The instances provided by producer methods are used to inject values to the fields inside a - * {@link com.github.kaktushose.jda.commands.annotations.interactions.Interaction Interaction} - * that are annotated with {@link Inject}. The access modifier of a producer method must be - * public. - * - *

Classes containing producer methods will be found automatically on startup. They can also be registered via - * {@link com.github.kaktushose.jda.commands.dependency.DependencyInjector#registerProvider(Object) - * DependencyInjector.registerProvider(Object)} - * - *

Please note that this is only a very basic implementation of dependency injection and can only be used inside - * interaction controller classes or custom {@link Implementation implementations}. Furthermore, each type can only - * have one producer. In other words you cannot register different instances of the same dependency. - * - * @see Inject - * @see com.github.kaktushose.jda.commands.dependency.DependencyInjector DependencyInjector - * @since 1.0.0 - */ +/// Methods annotated with Produces will be used to get the instance of a dependency. +/// +/// The instances provided by producer methods are used to inject values to the fields inside a [Interaction] +/// that are annotated with [Inject]. The access modifier of a producer method must be +/// public. +/// +/// Classes containing producer methods will be found automatically on startup. They can also be registered via +/// [DependencyInjector#registerProvider(Object)] +/// +/// Please note that this is only a very basic implementation of dependency injection and can only be used inside +/// interaction controller classes or custom [Implementation]s. Furthermore, each type can only +/// have one producer. In other words you cannot register different instances of the same dependency. +/// +/// @see Inject +/// @see com.github.kaktushose.jda.commands.dependency.DependencyInjector DependencyInjector @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Produces { - /** - * Whether jda-commands should ignore this method at indexing during startup. Useful if you wish to register your - * dependency providers manually by calling - * {@link com.github.kaktushose.jda.commands.dependency.DependencyInjector#registerProvider(Object) - * DependencyInjector#registerProvider(Object)} - * - * @return Whether jda-commands should ignore this method, default {@code true} - */ + /// Whether jda-commands should ignore this method at indexing during startup. Useful if you wish to register your + /// dependency providers manually by calling + /// [DependencyInjector#registerProvider(Object)] + /// + /// @return Whether jda-commands should ignore this method, default `true` boolean skipIndexing() default false; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Constraint.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Constraint.java index 3ed0aef29..e27e1eb5f 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Constraint.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Constraint.java @@ -5,21 +5,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Indicates that an annotation type can be used for parameter validation. When implementing custom validators, the - * annotation type must be annotated with this annotation. - * - * @see com.github.kaktushose.jda.commands.dispatching.validation.ValidatorRegistry ValidatorRegistry - * @since 2.0.0 - */ +/// Indicates that an annotation type can be used for parameter validation. When implementing custom validators, the +/// annotation type must be annotated with this annotation. +/// +/// @see com.github.kaktushose.jda.commands.dispatching.validation.ValidatorRegistry ValidatorRegistry @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Constraint { - /** - * Returns an array of all types this annotation can be used for. - * - * @return an array of all types this annotation can be used for - */ + /// Returns an array of all types this annotation can be used for. + /// + /// @return an array of all types this annotation can be used for Class[] value(); } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Max.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Max.java index 222a92f30..76ba440a8 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Max.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Max.java @@ -5,28 +5,21 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * The annotated element must be a number whose value must be lower or equal to the specified maximum. - * - * @see Constraint - * @since 2.0.0 - */ +/// The annotated element must be a number whose value must be less or equal to the specified maximum. +/// +/// @see Constraint @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Constraint({Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class}) public @interface Max { - /** - * Returns the value the element must be lower or equal to. - * - * @return Returns the value the element must be lower or equal to - */ + /// Returns the value the element must be less or equal to. + /// + /// @return Returns the value the element must be less or equal to long value(); - /** - * Returns the error message that will be displayed if the constraint fails. - * - * @return the error message - */ + /// Returns the error message that will be displayed if the constraint fails. + /// + /// @return the error message String message() default "Parameter exceeds maximum value"; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Min.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Min.java index 84af82013..7706fa846 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Min.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Min.java @@ -5,28 +5,21 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * The annotated element must be a number whose value must be greater or equal to the specified minimum. - * - * @see Constraint - * @since 2.0.0 - */ +/// The annotated element must be a number whose value must be greater or equal to the specified minimum. +/// +/// @see Constraint @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Constraint({Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class}) public @interface Min { - /** - * Returns the value the element must be greater or equal to. - * - * @return Returns the value the element must be greater or equal to - */ + /// Returns the value the element must be greater or equal to. + /// + /// @return Returns the value the element must be greater or equal to long value(); - /** - * Returns the error message that will be displayed if the constraint fails. - * - * @return the error message - */ + /// Returns the error message that will be displayed if the constraint fails. + /// + /// @return the error message String message() default "Parameter falls below minimum value"; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotPerm.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotPerm.java index 68e6fa8a7..8268572f2 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotPerm.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotPerm.java @@ -8,28 +8,21 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * The annotated element must be a user or member that doesn't have the specified discord permission. - * - * @see Constraint - * @since 2.0.0 - */ +/// The annotated element must be a user or member that **doesn't** have the specified discord permission. +/// +/// @see Constraint @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Constraint({Member.class, User.class}) public @interface NotPerm { - /** - * Returns the discord permission(s) the element must not have. - * - * @return the discord permission(s) the element must not have. - */ + /// Returns the discord permission(s) the element must not have. + /// + /// @return the discord permission(s) the element must not have. String[] value(); - /** - * Returns the error message that will be displayed if the constraint fails. - * - * @return the error message - */ + /// Returns the error message that will be displayed if the constraint fails. + /// + /// @return the error message String message() default "Member or User has at least one permission that isn't allowed"; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotRole.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotRole.java index c2b8c06f0..32adef342 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotRole.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotRole.java @@ -1,5 +1,6 @@ package com.github.kaktushose.jda.commands.annotations.constraints; +import com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry; import net.dv8tion.jda.api.entities.Member; import java.lang.annotation.ElementType; @@ -7,31 +8,23 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * The annotated element must be member that doesn't have the specified guild role. This constraint will use the - * {@link com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry TypeAdapterRegistry} to determine - * the role. - * - * @see Constraint - * @see com.github.kaktushose.jda.commands.dispatching.adapter.impl.RoleAdapter RoleAdapter - * @since 2.0.0 - */ +/// The annotated element must be member that **doesn't** have the specified guild role. This constraint will use the +/// [TypeAdapterRegistry] to determine the role. +/// +/// @see Constraint +/// @see com.github.kaktushose.jda.commands.dispatching.adapter.impl.RoleAdapter RoleAdapter @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Constraint({Member.class}) public @interface NotRole { - /** - * Returns the guild role the element must not have. - * - * @return the guild role the element must not have. - */ + /// Returns the guild role the element must not have. + /// + /// @return the guild role the element must not have. String value(); - /** - * Returns the error message that will be displayed if the constraint fails. - * - * @return the error message - */ + /// Returns the error message that will be displayed if the constraint fails. + /// + /// @return the error message String message() default "Member has at least one role that isn't allowed"; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotUser.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotUser.java index 1f55b3698..809e22045 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotUser.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/NotUser.java @@ -1,5 +1,7 @@ package com.github.kaktushose.jda.commands.annotations.constraints; +import com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry; +import com.github.kaktushose.jda.commands.dispatching.adapter.impl.RoleAdapter; import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.User; @@ -8,31 +10,23 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * The annotated element must not be the specified user or member. This constraint will use the - * {@link com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry TypeAdapterRegistry} to - * determine the user or member. - * - * @see Constraint - * @see com.github.kaktushose.jda.commands.dispatching.adapter.impl.RoleAdapter RoleAdapter - * @since 2.0.0 - */ +/// The annotated element must **not** be the specified user or member. This constraint will use the +/// [TypeAdapterRegistry] to determine the user or member. +/// +/// @see Constraint +/// @see RoleAdapter @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Constraint({Member.class, User.class}) public @interface NotUser { - /** - * Returns the user or member the element must not be equals to. - * - * @return the user or member the element must not be equals to - */ + /// Returns the user or member the element must not be equals to. + /// + /// @return the user or member the element must not be equals to String value(); - /** - * Returns the error message that will be displayed if the constraint fails. - * - * @return the error message - */ + /// Returns the error message that will be displayed if the constraint fails. + /// + /// @return the error message String message() default "The given Member or User is invalid as a parameter"; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Perm.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Perm.java index 59dc22409..bdf42ea4e 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Perm.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Perm.java @@ -8,28 +8,21 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * The annotated element must be a user or member that have the specified discord permission. - * - * @see Constraint - * @since 2.0.0 - */ +/// The annotated element must be a user or member that have the specified discord permission. +/// +/// @see Constraint @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Constraint({Member.class, User.class}) public @interface Perm { - /** - * Returns the discord permission(s) the element must have. - * - * @return the discord permission(s) the element must have. - */ + /// Returns the discord permission(s) the element must have. + /// + /// @return the discord permission(s) the element must have. String[] value(); - /** - * Returns the error message that will be displayed if the constraint fails. - * - * @return the error message - */ + /// Returns the error message that will be displayed if the constraint fails. + /// + /// @return the error message String message() default "Member or User is missing at least one permission that is required"; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Role.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Role.java index 0cfc01ef3..58a1a8108 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Role.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/Role.java @@ -1,5 +1,7 @@ package com.github.kaktushose.jda.commands.annotations.constraints; +import com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry; +import com.github.kaktushose.jda.commands.dispatching.adapter.impl.RoleAdapter; import net.dv8tion.jda.api.entities.Member; import java.lang.annotation.ElementType; @@ -7,30 +9,23 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * The annotated element must be member that have the specified guild role. This constraint will use the - * {@link com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry} to determine the role. - * - * @see Constraint - * @see com.github.kaktushose.jda.commands.dispatching.adapter.impl.RoleAdapter RoleAdapter - * @since 2.0.0 - */ +/// The annotated element must be member that have the specified guild role. This constraint will use the +/// [TypeAdapterRegistry] to determine the role. +/// +/// @see Constraint +/// @see RoleAdapter @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Constraint({Member.class}) public @interface Role { - /** - * Returns the guild role the element must have. - * - * @return the guild role the element must have. - */ + /// Returns the guild role the element must have. + /// + /// @return the guild role the element must have. String value(); - /** - * Returns the error message that will be displayed if the constraint fails. - * - * @return the error message - */ + /// Returns the error message that will be displayed if the constraint fails. + /// + /// @return the error message String message() default "Member or User is missing at least one role that is required"; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/User.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/User.java index 96c3e5f13..048cad8b1 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/User.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/User.java @@ -1,5 +1,7 @@ package com.github.kaktushose.jda.commands.annotations.constraints; +import com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry; +import com.github.kaktushose.jda.commands.dispatching.adapter.impl.RoleAdapter; import net.dv8tion.jda.api.entities.Member; import java.lang.annotation.ElementType; @@ -7,31 +9,23 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * The annotated element must be the specified user or member. This constraint will use the - * {@link com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry TypeAdapterRegistry} to determine - * the user or member. - * - * @see Constraint - * @see com.github.kaktushose.jda.commands.dispatching.adapter.impl.RoleAdapter RoleAdapter - * @since 2.0.0 - */ +/// The annotated element must be the specified user or member. This constraint will use the +/// [TypeAdapterRegistry] to determine the user or member. +/// +/// @see Constraint +/// @see RoleAdapter @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Constraint({Member.class, net.dv8tion.jda.api.entities.User.class}) public @interface User { - /** - * Returns the user or member the element must be equals to. - * - * @return the user or member the element must be equals to - */ + /// Returns the user or member the element must be equals to. + /// + /// @return the user or member the element must be equals to String value(); - /** - * Returns the error message that will be displayed if the constraint fails. - * - * @return the error message - */ + /// Returns the error message that will be displayed if the constraint fails. + /// + /// @return the error message String message() default "The given Member or User is invalid as a parameter"; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/package-info.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/package-info.java index 44c9ff02b..88abd9075 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/package-info.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/constraints/package-info.java @@ -1,4 +1,2 @@ -/** - * All annotations needed for parameter validation. - */ +/// All annotations needed for parameter validation. package com.github.kaktushose.jda.commands.annotations.constraints; diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/AutoComplete.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/AutoComplete.java index 5af595b39..28fa08b8f 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/AutoComplete.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/AutoComplete.java @@ -23,18 +23,14 @@ /// event.replyChoices(...); /// } /// ``` -/// /// @see SlashCommand -/// @since 4.0.0 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoComplete { - /** - * Returns the name of the slash commands this autocomplete should handle. - * - * @return the slash commands - */ + /// Returns the name of the slash commands this autocomplete should handle. + /// + /// @return the slash commands String[] value(); } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Button.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Button.java index 8d51a4073..61573d7d0 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Button.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Button.java @@ -28,37 +28,28 @@ /// ``` /// /// @see Interaction -/// @since 2.3.0 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Button { - /** - * Gets the label of the button. - * - * @return the label of the button - */ + /// Gets the label of the button. + /// + /// @return the label of the button String value() default ""; - /** - * Gets the {@link ButtonStyle}. - * - * @return the {@link ButtonStyle} - */ + /// Gets the [ButtonStyle]. + /// + /// @return the [ButtonStyle] ButtonStyle style() default ButtonStyle.PRIMARY; - /** - * Gets the {@link Emoji} of the button. - * - * @return the {@link Emoji} - */ + /// Gets the [Emoji] of the button. + /// + /// @return the [Emoji] String emoji() default ""; - /** - * Gets the link of the button. - * - * @return the link of the button - */ + /// Gets the link of the button. + /// + /// @return the link of the button String link() default ""; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Choices.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Choices.java index bc04a52c3..52f4ac2e4 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Choices.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Choices.java @@ -5,21 +5,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Annotation used to add choices to parameters. - * - * @see SlashCommand - * @since 2.3.0 - */ +/// Annotation used to add choices to parameters. +/// +/// @see SlashCommand @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface Choices { - /** - * Returns the choices of a parameter. This value will only be used for slash commands. - * - * @return the choices of the parameter - */ + /// Returns the choices of a parameter. This value will only be used for slash commands. + /// + /// @return the choices of the parameter String[] value(); } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/CommandScope.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/CommandScope.java new file mode 100644 index 000000000..e12319159 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/CommandScope.java @@ -0,0 +1,10 @@ +package com.github.kaktushose.jda.commands.annotations.interactions; + +/// Enum describing the two possible scopes a command can be registered for. +/// +/// @see SlashCommand#scope() +/// @see ContextCommand#scope() +public enum CommandScope { + GUILD, + GLOBAL +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/ContextCommand.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/ContextCommand.java index 602081004..56d498795 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/ContextCommand.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/ContextCommand.java @@ -28,59 +28,45 @@ /// @ContextCommand(value = "user context command", type = Command.Type.USER) /// public void onCommand(CommandEvent event, User target) { ... } /// ``` -/// /// @see Interaction -/// @since 4.0.0 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ContextCommand { - /** - * Returns the name of the command. - * - * @return the name of the command - */ + /// Returns the name of the command. + /// + /// @return the name of the command String value() default ""; - /** - * Returns whether this command is only usable in a guild. - * This only has an effect if this command is registered globally. - * - * @return {@code true} if this command is only usable in a guild - */ + /// Returns whether this command is only usable in a guild. + /// This only has an effect if this command is registered globally. + /// + /// @return `true` if this command is only usable in a guild boolean isGuildOnly() default false; - /** - * Returns whether this command can only be executed in NSFW channels. - * - * @return {@code true} if this command can only be executed in NSFW channels - */ + /// Returns whether this command can only be executed in NSFW channels. + /// + /// @return `true` if this command can only be executed in NSFW channels boolean isNSFW() default false; - /** - * Returns an array of {@link net.dv8tion.jda.api.Permission Permissions} this command - * should be enabled for by default. Note that guild admins can modify this at any time. - * - * @return an array of permissions this command will be enabled for by default - * @see Permissions Permission - * @see net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions DefaultMemberPermissions.ENABLED - * @see net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions DefaultMemberPermissions.DISABLED - */ + /// Returns an array of [Permission] this command + /// should be enabled for by default. Note that guild admins can modify this at any time. + /// + /// @return an array of permissions this command will be enabled for by default + /// @see Permissions Permission + /// @see net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions DefaultMemberPermissions.ENABLED + /// @see net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions DefaultMemberPermissions.DISABLED Permission[] enabledFor() default Permission.UNKNOWN; - /** - * Returns whether this command should be registered as a global or as a guild command. - * - * @return whether this command should be registered as a global or as a guild command - * @see SlashCommand.CommandScope - */ - SlashCommand.CommandScope scope() default SlashCommand.CommandScope.GLOBAL; + /// Returns whether this command should be registered as a global or as a guild command. + /// + /// @return whether this command should be registered as a global or as a guild command + /// @see CommandScope + CommandScope scope() default CommandScope.GLOBAL; - /** - * Gets the type of this command. - * - * @return the type of the command - */ + /// Gets the type of this command. + /// + /// @return the type of the command Command.Type type(); } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Cooldown.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Cooldown.java index c7c7f1dc4..6ffeac8dd 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Cooldown.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Cooldown.java @@ -6,28 +6,21 @@ import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; -/** - * Commands annotated with Cooldown will have a per-user cooldown. - * - * @see com.github.kaktushose.jda.commands.dispatching.middleware.impl.CooldownMiddleware CooldownMiddleware - * @since 1.0.0 - */ +/// Commands annotated with Cooldown will have a per-user cooldown. +/// +/// @see com.github.kaktushose.jda.commands.dispatching.middleware.impl.CooldownMiddleware CooldownMiddleware @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Cooldown { - /** - * Returns the delay of the cooldown. - * - * @return the delay of the cooldown - */ + /// Returns the delay of the cooldown. + /// + /// @return the delay of the cooldown long value(); - /** - * Returns the {@link TimeUnit} of the specified delay. - * - * @return the {@link TimeUnit} of the specified delay - */ + /// Returns the [TimeUnit] of the specified delay. + /// + /// @return the [TimeUnit] of the specified delay TimeUnit timeUnit(); } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/EntitySelectMenu.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/EntitySelectMenu.java index 5a68978fc..45b253854 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/EntitySelectMenu.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/EntitySelectMenu.java @@ -29,75 +29,60 @@ /// public void onMenu(ComponentEvent event, Mentions mentions) { ... } /// ``` /// @see Interaction -/// @since 2.3.0 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface EntitySelectMenu { - /** - * Supported {@link SelectTarget SelectTargets}. - * - * @return an array of supported {@link SelectTarget SelectTargets} - */ + /// Supported [SelectTarget]s. + /// + /// @return an array of supported [SelectTarget]s SelectTarget[] value(); - /** - * The {@link net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue default values} - * for roles that will be shown to the user. - * - * @return the {@link net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue default values} - */ + /// The [default values][net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue] + /// for roles that will be shown to the user. + /// + /// @return the [default values][net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue] long[] defaultRoles() default -1; - /** - * The {@link net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue default values} - * for channels that will be shown to the user. - * - * @return the {@link net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue default values} - */ + /// The [default values][net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue] + /// for channels that will be shown to the user. + /// + /// @return the [default values][net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue] long[] defaultChannels() default -1; - /** - * The {@link net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue default values} - * for users that will be shown to the user. - * - * @return the {@link net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue default values} - */ + /// The [default values][net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue] + /// for users that will be shown to the user. + /// + /// @return the [default values][net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue] long[] defaultUsers() default -1; - /** - * The {@link ChannelType ChannelTypes} that should be supported by this menu. - * This is only relevant for menus that allow CHANNEL targets. - * - * @return the {@link ChannelType ChannelTypes} that should be supported by this menu - */ + /// The [ChannelType]s that should be supported by this menu. + /// This is only relevant for menus that allow CHANNEL targets. + /// + /// @return the [ChannelType]s that should be supported by this menu ChannelType[] channelTypes() default ChannelType.UNKNOWN; - /** - * Configure the placeholder which is displayed when no selections have been made yet. - * - * @return the placeholder which is displayed when no selections have been made yet - */ + /// Configure the placeholder which is displayed when no selections have been made yet. + /// + /// @return the placeholder which is displayed when no selections have been made yet String placeholder() default ""; - /** - * The minimum amount of values a user has to select. - *
Default: {@code 1} - * - *

The minimum must not exceed the amount of available options. - * - * @return the minimum amount of values a user has to select - */ + /// The minimum amount of values a user has to select. + /// + /// Default: `1` + /// + /// The minimum must not exceed the amount of available options. + /// + /// @return the minimum amount of values a user has to select int minValue() default 1; - /** - * The maximum amount of values a user can select. - *
Default: {@code 1} - * - *

The maximum must not exceed the amount of available options. - * - * @return the maximum amount of values a user can select - */ + /// The maximum amount of values a user can select. + /// + /// Default: `1` + /// + /// The maximum must not exceed the amount of available options. + /// + /// @return the maximum amount of values a user can select int maxValue() default 1; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Interaction.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Interaction.java index 32c010799..4af52daf6 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Interaction.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Interaction.java @@ -22,17 +22,13 @@ /// } /// } /// ``` -/// -/// @since 4.0.0 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Interaction { - /** - * Returns the base name for slash commands. - * - * @return the base name for slash commands - */ + /// Returns the base name for slash commands. + /// + /// @return the base name for slash commands String value() default ""; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Modal.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Modal.java index a9d86b105..ca56dbfbe 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Modal.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Modal.java @@ -19,19 +19,15 @@ /// @Modal("My Modal") /// public void onModal(ModalEvent event, @TextInput("Type here") String input) { ... } /// ``` -/// /// @see Interaction /// @see TextInput -/// @since 4.0.0 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Modal { - /** - * Gets the title of this modal. - * - * @return the title of the modal - */ + /// Gets the title of this modal. + /// + /// @return the title of the modal String value(); } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Optional.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Optional.java index 0e5fc05b6..ecf9c1860 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Optional.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Optional.java @@ -1,33 +1,28 @@ package com.github.kaktushose.jda.commands.annotations.interactions; +import com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Parameters annotated with Optional are as the name says optional. - * - *

More formally if a command has an optional parameter the argument doesn't need to be present to execute the - * command. - * - *

It is also possible to pass a default value which will be used instead if the argument isn't present. - * The default value will be handled as a normal input and thus the - * {@link com.github.kaktushose.jda.commands.dispatching.adapter.TypeAdapterRegistry TypeAdapterRegistry} - * will try to parse it. If the parsing fails the command will still be executed but with empty or - * possible {@code null} values. - * - * @see SlashCommand - * @since 1.0.0 - */ +/// Parameters annotated with Optional are as the name says optional. +/// +/// More formally if a command has an optional parameter the argument doesn't need to be present to execute the +/// command. +/// +/// It is also possible to pass a default value which will be used instead if the argument isn't present. +/// The default value will be handled as a normal input and thus the [TypeAdapterRegistry] will try to parse it. +/// If the parsing fails the command will still be executed but with empty or possible `null` values. +/// +/// @see SlashCommand @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface Optional { - /** - * Returns the default value. - * - * @return the default value - */ + /// Returns the default value. + /// + /// @return the default value String value() default ""; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Param.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Param.java index 43077100f..97fdff79d 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Param.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Param.java @@ -5,29 +5,22 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Annotation used to add a name and description to slash command parameters. - * - * @see SlashCommand - * @since 2.3.0 - */ +/// Annotation used to add a name and description to slash command parameters. +/// +/// @see SlashCommand @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface Param { - /** - * Returns the description of the parameter. This value will only be used for slash commands. - * - * @return the description of the parameter - */ + /// Returns the description of the parameter. This value will only be used for slash commands. + /// + /// @return the description of the parameter String value() default ""; - /** - * Returns the name of the parameter. Use the compiler flag {@code -parameters} to have the parameter name resolved - * at runtime making this field redundant. - * - * @return the name of the parameter - */ + /// Returns the name of the parameter. Use the compiler flag `-parameters` to have the parameter name resolved + /// at runtime making this field redundant. + /// + /// @return the name of the parameter String name() default ""; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Permissions.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Permissions.java index 792a564a8..99940d58d 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Permissions.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/Permissions.java @@ -1,33 +1,38 @@ package com.github.kaktushose.jda.commands.annotations.interactions; +import com.github.kaktushose.jda.commands.dispatching.middleware.impl.PermissionsMiddleware; +import com.github.kaktushose.jda.commands.permissions.PermissionsProvider; +import net.dv8tion.jda.api.Permission; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * {@link com.github.kaktushose.jda.commands.annotations.interactions.Interaction Interaction} classes or - * interaction methods (commands, components or modals) annotated with Permission will - * require the user to have the given permissions in order to execute the command. - * - *

The default implementation of this framework can only handle discord permissions. - * However, the {@link com.github.kaktushose.jda.commands.permissions.PermissionsProvider PermissionsProvider} interface - * allows custom implementations. - * - *

If a class is annotated with Permission all methods or respectively all interactions will require the given - * permission level. - * - * @see com.github.kaktushose.jda.commands.permissions.PermissionsProvider PermissionsProvider - * @since 1.0.0 - */ +/// [Interaction] classes or +/// interaction methods (commands, components or modals) annotated with Permission will +/// require the user to have the given permissions in order to execute the command. +/// +/// @apiNote This annotation should not be confused with [SlashCommand#enabledFor()] or [ContextCommand#enabledFor()]. +/// The `enabledFor` permissions will be client-side checked by Discord directly, while the [Permissions] annotation +/// will be used for the own permission system of jda-commands. +/// @implNote The [PermissionsMiddleware] will validate the permissions during the middleware phase of the execution +/// chain. The [PermissionsProvider] will be called to check the respective user. The default implementation of the +/// [PermissionsProvider] can only handle Discord permissions([Permission]). +/// +/// ## Example: +/// ``` +/// @Permissions("BAN_MEMBERS") +/// public class BanCommand { ... } +/// ``` +/// +/// @see PermissionsProvider @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Permissions { - /** - * Returns a String array of all required permissions. - * - * @return a String array of all required permissions. - */ + /// Returns a String array of all required permissions. + /// + /// @return a String array of all required permissions. String[] value(); } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/ReplyConfig.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/ReplyConfig.java index d624a0b74..6189759b6 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/ReplyConfig.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/ReplyConfig.java @@ -26,7 +26,6 @@ /// 4. [GlobalReplyConfig] /// /// @see ReplyableEvent -/// @since 4.0.0 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ReplyConfig { diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/SelectOption.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/SelectOption.java index de03b476e..e1b7f26c3 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/SelectOption.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/SelectOption.java @@ -5,51 +5,38 @@ import java.lang.annotation.*; -/** - * Used to define {@link net.dv8tion.jda.api.interactions.components.selections.SelectOption SelectOptions} - * for {@link StringSelectMenu StringSelectMenus}. - * - * @see StringSelectMenu - * @since 4.0.0 - */ +/// Used to define [`SelectOptions`][net.dv8tion.jda.api.interactions.components.selections.SelectOption] +/// for [StringSelectMenu]s. +/// +/// @see StringSelectMenu @Repeatable(SelectOptionContainer.class) @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SelectOption { - /** - * Gets the label of an option. - * - * @return the label - */ + /// Gets the label of an option. + /// + /// @return the label String label(); - /** - * Gets the value of an option. - * - * @return the value - */ + /// Gets the value of an option. + /// + /// @return the value String value(); - /** - * Gets the description of an option. - * - * @return the description - */ + /// Gets the description of an option. + /// + /// @return the description String description() default ""; - /** - * Gets the {@link Emoji} of an option. - * - * @return the {@link Emoji} - */ + /// Gets the [Emoji] of an option. + /// + /// @return the [Emoji] String emoji() default ""; - /** - * Gets whether this option is a default option. - * - * @return {@code true} if this SelectOption is a default option - */ + /// Gets whether this option is a default option. + /// + /// @return `true` if this SelectOption is a default option boolean isDefault() default false; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/SlashCommand.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/SlashCommand.java index 177f3786d..de40f4898 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/SlashCommand.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/SlashCommand.java @@ -37,72 +37,47 @@ /// public void onCommand(CommandEvent event, String[] arguments) {} /// } /// ``` -/// /// @see Interaction /// @see com.github.kaktushose.jda.commands.annotations.interactions.Interaction Interaction /// @see com.github.kaktushose.jda.commands.annotations.constraints.Constraint Constraint -/// @since 1.0.0 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SlashCommand { - /** - * Returns the name of the command. - * - * @return the name of the command - */ + /// Returns the name of the command. + /// + /// @return the name of the command String value() default ""; - /** - * Returns whether this command is only usable in a guild. - * This only has an effect if this command is registered globally. - * - * @return {@code true} if this command is only usable in a guild - */ + /// Returns whether this command is only usable in a guild. + /// This only has an effect if this command is registered globally. + /// + /// @return `true` if this command is only usable in a guild boolean isGuildOnly() default false; - /** - * Returns the description of the command. - * - * @return the description of the command - */ + /// Returns the description of the command. + /// + /// @return the description of the command String desc() default "N/A"; - /** - * Returns whether this command can only be executed in NSFW channels. - * - * @return {@code true} if this command can only be executed in NSFW channels - */ + /// Returns whether this command can only be executed in NSFW channels. + /// + /// @return `true` if this command can only be executed in NSFW channels boolean isNSFW() default false; - /** - * Returns an array of {@link net.dv8tion.jda.api.Permission Permissions} this command - * should be enabled for by default. Note that guild admins can modify this at any time. - * - * @return an array of permissions this command will be enabled for by default - * @see Permissions Permission - * @see net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions DefaultMemberPermissions.ENABLED - * @see net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions DefaultMemberPermissions.DISABLED - */ + /// Returns an array of [Permission]s this command should be enabled for by default. Note that guild admins can + /// modify this at any time. + /// + /// @return an array of permissions this command will be enabled for by default + /// @see Permissions Permission + /// @see net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions DefaultMemberPermissions.ENABLED + /// @see net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions DefaultMemberPermissions.DISABLED Permission[] enabledFor() default Permission.UNKNOWN; - /** - * Returns whether this command should be registered as a global or as a guild command. - * - * @return whether this command should be registered as a global or as a guild command - * @see CommandScope - */ + /// Returns whether this command should be registered as a global or as a guild command. + /// + /// @return whether this command should be registered as a global or as a guild command + /// @see CommandScope CommandScope scope() default CommandScope.GLOBAL; - /** - * Enum describing the two possible scopes a command can be registered for. - * - * @see SlashCommand#scope() - * @since 4.0.0 - */ - enum CommandScope { - GUILD, - GLOBAL - } - } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/StringSelectMenu.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/StringSelectMenu.java index 688f622a6..73ce8ce78 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/StringSelectMenu.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/StringSelectMenu.java @@ -31,36 +31,31 @@ /// ``` /// @see Interaction /// @see SelectOption -/// @since 4.0.0 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface StringSelectMenu { - /** - * Configure the placeholder which is displayed when no selections have been made yet. - * - * @return the placeholder which is displayed when no selections have been made yet - */ + /// Configure the placeholder which is displayed when no selections have been made yet. + /// + /// @return the placeholder which is displayed when no selections have been made yet String value(); - /** - * The minimum amount of values a user has to select. - *
Default: {@code 1} - * - *

The minimum must not exceed the amount of available options. - * - * @return the minimum amount of values a user has to select - */ + /// The minimum amount of values a user has to select. + /// + /// Default: `1` + /// + /// The minimum must not exceed the amount of available options. + /// + /// @return the minimum amount of values a user has to select int minValue() default 1; - /** - * The maximum amount of values a user can select. - *
Default: {@code 1} - * - *

The maximum must not exceed the amount of available options. - * - * @return the maximum amount of values a user can select - */ + /// The maximum amount of values a user can select. + /// + /// Default: `1` + /// + /// The maximum must not exceed the amount of available options. + /// + /// @return the maximum amount of values a user can select int maxValue() default 1; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/TextInput.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/TextInput.java index c1de71bc1..156b597b1 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/TextInput.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/interactions/TextInput.java @@ -7,67 +7,53 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Annotation used to add TextInputs to {@link Modal Modals}. - * - * @see Modal - * @since 4.0.0 - */ +/// Annotation used to add TextInputs to [Modal]s. +/// +/// @see Modal @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface TextInput { - /** - * The placeholder of this TextInput - *
This is the short hint that describes the expected value of the TextInput field. - * - * @return Placeholder - */ + /// The placeholder of this TextInput + /// + /// This is the short hint that describes the expected value of the TextInput field. + /// + /// @return Placeholder String value(); - /** - * The label shown above this text input box - * - * @return Label for the input - */ + /// The label shown above this text input box + /// + /// @return Label for the input String label() default ""; - /** - * The default value of this TextInput. - *
This sets a pre-populated text for this TextInput field - * - * @return default value - */ + /// The default value of this TextInput. + /// + /// This sets a pre-populated text for this TextInput field + /// + /// @return default value String defaultValue() default ""; - /** - * The minimum length. This is -1 if none has been set. - * - * @return Minimum length or -1 - */ + /// The minimum length. This is -1 if none has been set. + /// + /// @return Minimum length or -1 int minValue() default -1; - /** - * The maximum length. This is -1 if none has been set. - * - * @return Maximum length or -1 - */ + /// The maximum length. This is -1 if none has been set. + /// + /// @return Maximum length or -1 int maxValue() default -1; - /** - * The {@link TextInputStyle TextInputStyle}. The default value is {@link TextInputStyle#PARAGRAPH}. - * - * @return The TextInputStyle - */ + /// The [TextInputStyle]. The default value is [TextInputStyle#PARAGRAPH]. + /// + /// @return The TextInputStyle TextInputStyle style() default TextInputStyle.PARAGRAPH; - /** - * Whether this TextInput is required. - *
If this is True, the user must populate this TextInput field before they can submit the Modal. - * - * @return True if this TextInput is required - * @see net.dv8tion.jda.api.interactions.components.text.TextInput#isRequired() - */ + /// Whether this TextInput is required. + /// + /// If this is True, the user must populate this TextInput field before they can submit the Modal. + /// + /// @return True if this TextInput is required + /// @see net.dv8tion.jda.api.interactions.components.text.TextInput#isRequired() boolean required() default true; } diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/internal/SelectOptionContainer.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/internal/SelectOptionContainer.java index beab34365..83866dd03 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/internal/SelectOptionContainer.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/internal/SelectOptionContainer.java @@ -1,20 +1,19 @@ package com.github.kaktushose.jda.commands.annotations.internal; import com.github.kaktushose.jda.commands.annotations.interactions.SelectOption; +import org.jetbrains.annotations.ApiStatus; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -/** - * Internal wrapper for repeating {@link SelectOption SelectOptions}. - * - * @see SelectOption - * @since 4.0.0 - */ +/// Internal wrapper for repeating [SelectOption]s. +/// +/// @see SelectOption @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) +@ApiStatus.Internal public @interface SelectOptionContainer { SelectOption[] value(); diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/internal/package-info.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/internal/package-info.java index 91bae5373..7c13bc512 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/internal/package-info.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/internal/package-info.java @@ -1,4 +1,2 @@ -/** - * Annotations that are only used for internal purposes and aren't intended to use in production. - */ +/// Annotations that are only used for internal purposes and aren't intended to use in production. package com.github.kaktushose.jda.commands.annotations.internal; diff --git a/src/main/java/com/github/kaktushose/jda/commands/annotations/package-info.java b/src/main/java/com/github/kaktushose/jda/commands/annotations/package-info.java index 9b50e4c46..9485e3173 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/annotations/package-info.java +++ b/src/main/java/com/github/kaktushose/jda/commands/annotations/package-info.java @@ -1,4 +1,2 @@ -/** - * All annotations that are needed to use this framework. - */ +/// All annotations that are needed to use this framework. package com.github.kaktushose.jda.commands.annotations; diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/Definition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/Definition.java new file mode 100644 index 000000000..a1ec7cca8 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/Definition.java @@ -0,0 +1,27 @@ +package com.github.kaktushose.jda.commands.definitions; + +import com.github.kaktushose.jda.commands.definitions.features.CustomIdJDAEntity; +import com.github.kaktushose.jda.commands.definitions.features.JDAEntity; +import com.github.kaktushose.jda.commands.definitions.features.internal.Invokable; +import com.github.kaktushose.jda.commands.definitions.interactions.InteractionDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.ModalDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.ParameterDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.SlashCommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.StringSelectMenuDefinition; +import org.jetbrains.annotations.NotNull; + +/// The common interface for all interaction definitions and their sub parts, such as parameters or text inputs, etc. +public sealed interface Definition permits CustomIdJDAEntity, Invokable, JDAEntity, InteractionDefinition, + ModalDefinition.TextInputDefinition, ParameterDefinition, ParameterDefinition.ConstraintDefinition, + SlashCommandDefinition.CooldownDefinition, StringSelectMenuDefinition.SelectOptionDefinition { + + /// The id for this definition. Per default this is the hash code of the [#toString()] method. + @NotNull + default String definitionId() { + return String.valueOf(toString().hashCode()); + } + + /// The human-readable name of this definition. + @NotNull + String displayName(); +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/ClassDescription.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/ClassDescription.java new file mode 100644 index 000000000..e64468bc6 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/ClassDescription.java @@ -0,0 +1,19 @@ +package com.github.kaktushose.jda.commands.definitions.description; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; +import java.util.Collection; + +/// A [Description] that describes a class. +/// +/// @param clazz the [Class] this [Description] describes. +/// @param name the name of the class +/// @param annotations a [Collection] of all [Annotation]s this class is annotated with +/// @param methods a [Collection] of all the public [`methods`][MethodDescription] of this class +public record ClassDescription( + @NotNull Class clazz, + @NotNull String name, + @NotNull Collection annotations, + @NotNull Collection methods +) implements Description {} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Description.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Description.java new file mode 100644 index 000000000..a12c469d7 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Description.java @@ -0,0 +1,31 @@ +package com.github.kaktushose.jda.commands.definitions.description; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Optional; + +/// The common interface of all [Description] types. +/// +/// A [Description] is, as the name says, a description of a class, method or parameter, similar to how +/// `java.lang.reflect` works. +/// +/// @see ClassDescription +/// @see MethodDescription +/// @see ParameterDescription +public sealed interface Description permits ClassDescription, MethodDescription, ParameterDescription { + + /// a possibly-empty [Collection] of all [Annotation]s this element is annotated with. + @NotNull + Collection annotations(); + + /// Gets this element's [Annotation] for the specified type if such an annotation is present + /// + /// @param type the type of the annotation to get + /// @return an [Optional] holding the [Annotation] if present at this element or else an empty [Optional] + @NotNull + default Optional annotation(@NotNull Class type) { + return annotations().stream().filter(type::isInstance).map(type::cast).findFirst(); + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Descriptor.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Descriptor.java new file mode 100644 index 000000000..34fdcd01b --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Descriptor.java @@ -0,0 +1,8 @@ +package com.github.kaktushose.jda.commands.definitions.description; + +import java.util.function.Function; + +/// A [Descriptor] takes a [Class] as input and transforms it into a [ClassDescription]. +@FunctionalInterface +public interface Descriptor extends Function, ClassDescription> { +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Invoker.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Invoker.java new file mode 100644 index 000000000..0d956aa54 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/Invoker.java @@ -0,0 +1,20 @@ +package com.github.kaktushose.jda.commands.definitions.description; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationTargetException; +import java.util.SequencedCollection; + +/// A functional interface, that allows the invocation of a [MethodDescription]. +@FunctionalInterface +public interface Invoker { + + /// @param instance an instance of the declaring class of the method + /// @param arguments a [SequencedCollection] of the arguments the method should be invoked with + /// @return the result of the method invocation + /// @throws IllegalAccessException if this Method object is enforcing Java language access control and the + /// underlying method is inaccessible + /// @throws InvocationTargetException if an exception was thrown by the invoked method or constructor + @Nullable Object invoke(@NotNull Object instance, @NotNull SequencedCollection arguments) throws IllegalAccessException, InvocationTargetException; +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/MethodDescription.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/MethodDescription.java new file mode 100644 index 000000000..acc28bcc2 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/MethodDescription.java @@ -0,0 +1,25 @@ +package com.github.kaktushose.jda.commands.definitions.description; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.SequencedCollection; + +/// A [Description] that describes a method. +/// +/// @param declaringClass the declaring [Class] of this method +/// @param returnType the [Class] this method returns +/// @param name the name of the method +/// @param parameters a [SequencedCollection] of the [ParameterDescription]s of this method +/// @param annotations a [Collection] of all [Annotation]s this method is annotated with +/// @param invoker the corresponding [Invoker], used to invoke this method +public record MethodDescription( + @NotNull Class declaringClass, + @NotNull Class returnType, + @NotNull String name, + @NotNull SequencedCollection parameters, + @NotNull Collection annotations, + @NotNull Invoker invoker +) implements Description { +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/ParameterDescription.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/ParameterDescription.java new file mode 100644 index 000000000..3816c2273 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/ParameterDescription.java @@ -0,0 +1,17 @@ +package com.github.kaktushose.jda.commands.definitions.description; + +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; +import java.util.Collection; + +/// A [Description] that describes a parameter. +/// +/// @param type the [Class] representing the type of this parameter +/// @param name the name of the parameter +/// @param annotations a [Collection] of all [Annotation]s this parameter is annotated with +public record ParameterDescription( + @NotNull Class type, + @NotNull String name, + @NotNull Collection annotations +) implements Description {} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/package-info.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/package-info.java new file mode 100644 index 000000000..da7dd094b --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/package-info.java @@ -0,0 +1,6 @@ +/// This packages includes different types of [`Descriptions`][com.github.kaktushose.jda.commands.definitions.description.Description] +/// that contain meta information about classes, methods und parameters, similar to reflections. +/// +/// Descriptions are created by an implementation of [`Descriptions`][com.github.kaktushose.jda.commands.definitions.description.Description] +/// and are used in many places of this framework, resulting in the possible avoidance of java reflections. +package com.github.kaktushose.jda.commands.definitions.description; diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/ReflectiveClassFinder.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/ReflectiveClassFinder.java new file mode 100644 index 000000000..3da9ea859 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/ReflectiveClassFinder.java @@ -0,0 +1,28 @@ +package com.github.kaktushose.jda.commands.definitions.description.reflective; + +import com.github.kaktushose.jda.commands.annotations.interactions.Interaction; +import org.jetbrains.annotations.ApiStatus; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; + +// temp class +@ApiStatus.Internal +public class ReflectiveClassFinder { + + public static Iterable> find(Class clazz, String[] packages) { + var filter = new FilterBuilder(); + for (String pkg : packages) { + filter.includePackage(pkg); + } + + var config = new ConfigurationBuilder() + .setScanners(Scanners.SubTypes, Scanners.TypesAnnotated) + .setUrls(ClasspathHelper.forClass(clazz)) + .filterInputsBy(filter); + var reflections = new Reflections(config); + return reflections.getTypesAnnotatedWith(Interaction.class); + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/ReflectiveDescriptor.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/ReflectiveDescriptor.java new file mode 100644 index 000000000..f289f3d9d --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/ReflectiveDescriptor.java @@ -0,0 +1,72 @@ +package com.github.kaktushose.jda.commands.definitions.description.reflective; + +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.Descriptor; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.definitions.description.ParameterDescription; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +/// An [Descriptor] implementation that uses `java.lang.reflect` to create the [ClassDescription]. +public class ReflectiveDescriptor implements Descriptor { + + /// Transforms the given [Class] into a [ClassDescription]. + /// + /// @param clazz the [Class] to transform + /// @return the [ClassDescription] built from the given [Class] + @NotNull + @Override + public ClassDescription apply(@NotNull Class clazz) { + List methods = Arrays.stream(clazz.getMethods()) + .map(this::method) + .filter(Objects::nonNull) + .toList(); + + return new ClassDescription( + clazz, + clazz.getName(), + toList(clazz.getAnnotations()), + methods + ); + } + + @Nullable + private MethodDescription method(@NotNull Method method) { + if (!Modifier.isPublic(method.getModifiers())) return null; + List parameters = Arrays.stream(method.getParameters()) + .map(this::parameter) + .toList(); + + + return new MethodDescription( + method.getDeclaringClass(), + method.getReturnType(), + method.getName(), + parameters, + toList(method.getAnnotations()), + (instance, arguments) -> method.invoke(instance, arguments.toArray()) + ); + } + + @NotNull + private ParameterDescription parameter(@NotNull Parameter parameter) { + return new ParameterDescription( + parameter.getType(), + parameter.getName(), + toList(parameter.getAnnotations()) + ); + } + + @NotNull + private Collection toList(@NotNull T[] array) { + return Arrays.stream(array).toList(); + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/package-info.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/package-info.java new file mode 100644 index 000000000..1a7e340de --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/description/reflective/package-info.java @@ -0,0 +1,3 @@ +/// Contains [`Descriptors`][com.github.kaktushose.jda.commands.definitions.description.Descriptor] that use +/// `java.lang.reflect` to build [`Descriptions][com.github.kaktushose.jda.commands.definitions.description.Description]. +package com.github.kaktushose.jda.commands.definitions.description.reflective; diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/features/CustomIdJDAEntity.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/features/CustomIdJDAEntity.java new file mode 100644 index 000000000..b4d76995a --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/features/CustomIdJDAEntity.java @@ -0,0 +1,27 @@ +package com.github.kaktushose.jda.commands.definitions.features; + +import com.github.kaktushose.jda.commands.definitions.Definition; +import com.github.kaktushose.jda.commands.definitions.interactions.CustomId; +import com.github.kaktushose.jda.commands.definitions.interactions.ModalDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.ButtonDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.ComponentDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.EntitySelectMenuDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.StringSelectMenuDefinition; +import org.jetbrains.annotations.NotNull; + +/// Indicates that the implementing [Definition] can be transformed into a JDA entity that has a [CustomId]. +/// +/// @see ButtonDefinition +/// @see EntitySelectMenuDefinition +/// @see StringSelectMenuDefinition +/// @see ModalDefinition +public sealed interface CustomIdJDAEntity extends Definition permits ComponentDefinition, ModalDefinition { + + /// Transforms this [Definition] into a JDA entity of the given type [T]. + /// + /// @param customId the [CustomId] to use to build this JDA entity + /// @return a JDA entity of type [T] + @NotNull + T toJDAEntity(@NotNull CustomId customId); + +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/features/JDAEntity.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/features/JDAEntity.java new file mode 100644 index 000000000..dd2fd6202 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/features/JDAEntity.java @@ -0,0 +1,35 @@ +package com.github.kaktushose.jda.commands.definitions.features; + +import com.github.kaktushose.jda.commands.definitions.Definition; +import com.github.kaktushose.jda.commands.definitions.interactions.ModalDefinition.TextInputDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.CommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.ContextCommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.ParameterDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.SlashCommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.ButtonDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.ComponentDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.EntitySelectMenuDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.StringSelectMenuDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.StringSelectMenuDefinition.SelectOptionDefinition; +import org.jetbrains.annotations.NotNull; + +/// Indicates that the implementing [Definition] can be transformed into a JDA entity. +/// +/// @see ButtonDefinition +/// @see EntitySelectMenuDefinition +/// @see StringSelectMenuDefinition +/// @see SlashCommandDefinition +/// @see ContextCommandDefinition +/// @see ParameterDefinition +/// @see SelectOptionDefinition +/// @see TextInputDefinition +public sealed interface JDAEntity extends Definition + permits ComponentDefinition, TextInputDefinition, CommandDefinition, ParameterDefinition, SelectOptionDefinition { + + /// Transforms this [Definition] into a JDA entity of the given type [T]. + /// + /// @return a JDA entity of type [T] + @NotNull + T toJDAEntity(); + +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/features/internal/Invokable.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/features/internal/Invokable.java new file mode 100644 index 000000000..0ee833dae --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/features/internal/Invokable.java @@ -0,0 +1,51 @@ +package com.github.kaktushose.jda.commands.definitions.features.internal; + +import com.github.kaktushose.jda.commands.definitions.Definition; +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.definitions.interactions.InteractionDefinition; +import com.github.kaktushose.jda.commands.dispatching.context.InvocationContext; +import com.github.kaktushose.jda.commands.dispatching.handling.EventHandler; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; +import java.util.SequencedCollection; + +/// Indicates that the implementing [Definition] is bound to a method that can be invoked. +@ApiStatus.Internal +public sealed interface Invokable extends Definition permits InteractionDefinition { + + Logger log = LoggerFactory.getLogger(Invokable.class); + + /// Invokes the method that this [Definition] is bound to. + /// + /// @param instance an instance of the declaring class of the method + /// @param invocation the corresponding [InvocationContext] + /// @return the result of the method invocation + /// @throws IllegalStateException if the definition must not be invoked at the moment this method gets called + /// @throws IllegalAccessException if the method object is enforcing Java language access control and the + /// underlying method is inaccessible + /// @throws InvocationTargetException if an exception was thrown by the invoked method or constructor + @Nullable + default Object invoke(@NotNull Object instance, @NotNull InvocationContext invocation) throws IllegalAccessException, InvocationTargetException { + if (!EventHandler.INVOCATION_PERMITTED.get()) { + throw new IllegalStateException("The Definition must not be invoked at this point."); + } + SequencedCollection arguments = invocation.arguments(); + + EventHandler.INVOCATION_PERMITTED.set(false); + return methodDescription().invoker().invoke(instance, arguments); + } + + /// The [ClassDescription] of the declaring class of the [#methodDescription()]. + @NotNull + ClassDescription clazzDescription(); + + /// The [MethodDescription] of the method this [Definition] is bound to. + @NotNull + MethodDescription methodDescription(); +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/features/package-info.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/features/package-info.java new file mode 100644 index 000000000..d4202ea90 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/features/package-info.java @@ -0,0 +1,2 @@ +/// This package contains interfaces that implement some form of functionality for a definition, that not all definitions might share. +package com.github.kaktushose.jda.commands.definitions.features; \ No newline at end of file diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/AutoCompleteDefinition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/AutoCompleteDefinition.java new file mode 100644 index 000000000..bb60afb20 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/AutoCompleteDefinition.java @@ -0,0 +1,51 @@ +package com.github.kaktushose.jda.commands.definitions.interactions; + +import com.github.kaktushose.jda.commands.annotations.interactions.AutoComplete; +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.dispatching.events.interactions.AutoCompleteEvent; +import com.github.kaktushose.jda.commands.internal.Helpers; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +/// Representation of an auto complete handler. +/// +/// @param clazzDescription the [ClassDescription] of the declaring class of the [#methodDescription()] +/// @param methodDescription the [MethodDescription] of the method this definition is bound to +/// @param commands the commands this autocomplete handler can handle +public record AutoCompleteDefinition(@NotNull ClassDescription clazzDescription, + @NotNull MethodDescription methodDescription, + @NotNull Set commands) + implements InteractionDefinition { + + /// Builds a new [AutoCompleteDefinition] from the given class and method description. + /// + /// @param clazz the corresponding [ClassDescription] + /// @param method the corresponding [MethodDescription] + /// @return an [Optional] holding the [AutoCompleteDefinition] + @NotNull + public static Optional build(@NotNull ClassDescription clazz, @NotNull MethodDescription method) { + if (Helpers.checkSignature(method, List.of(AutoCompleteEvent.class))) { + return Optional.empty(); + } + return method.annotation(AutoComplete.class).map(complete -> + new AutoCompleteDefinition(clazz, method, Arrays.stream(complete.value()).collect(Collectors.toSet())) + ); + + } + + @NotNull + @Override + public String displayName() { + return "%s.%s".formatted(methodDescription.declaringClass().getName(), methodDescription.name()); + } + + @NotNull + @Override + public Collection permissions() { + return Collections.emptyList(); + } + +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/CustomId.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/CustomId.java new file mode 100644 index 000000000..979f83798 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/CustomId.java @@ -0,0 +1,105 @@ +package com.github.kaktushose.jda.commands.definitions.interactions; + +import com.github.kaktushose.jda.commands.definitions.Definition; +import net.dv8tion.jda.api.events.interaction.GenericInteractionCreateEvent; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.GenericComponentInteractionCreateEvent; +import org.jetbrains.annotations.NotNull; + +/// Representation of a custom id used in modals, buttons or select menus. +/// +/// @param runtimeId the id of the [`Runtime`]({@docRoot}/index.html#runtime-concept-heading) this custom id is bound to +/// or the literal `independent`. +/// @param definitionId the [Definition#definitionId()] +/// @implNote the custom id has the following format: `jdac.runtimeId.definitionId` +public record CustomId(@NotNull String runtimeId, @NotNull String definitionId) { + private static final String PREFIX = "jdac"; + private static final String INDEPENDENT_ID = "independent"; + public static final String BOUND_CUSTOM_ID_REGEX = "^jdac\\.[0-9a-fA-F-]{36}\\.-?\\d+$"; + public static final String INDEPENDENT_CUSTOM_ID_REGEX = "^jdac\\.%s\\.-?\\d+$".formatted(INDEPENDENT_ID); + + public CustomId { + if (!runtimeId.matches("[0-9a-fA-F-]{36}") && !runtimeId.equals(INDEPENDENT_ID)) { + throw new IllegalArgumentException("Invalid runtime id! Must either be a UUID or \"%s\"".formatted(INDEPENDENT_ID)); + } + } + + /// Constructs a new [CustomId] from the given [GenericInteractionCreateEvent]. + /// + /// @param event the [GenericInteractionCreateEvent] + /// @return the [CustomId] + @NotNull + public static CustomId fromEvent(@NotNull GenericComponentInteractionCreateEvent event) { + return fromEvent(event.getComponentId()); + } + + /// Constructs a new [CustomId] from the given [ModalInteractionEvent]. + /// + /// @param event the [ModalInteractionEvent] + /// @return the [CustomId] + @NotNull + public static CustomId fromEvent(@NotNull ModalInteractionEvent event) { + return fromEvent(event.getModalId()); + } + + /// Constructs a new [CustomId] from the given String. + /// + /// @param customId the custom id String + /// @return the [CustomId] + @NotNull + private static CustomId fromEvent(@NotNull String customId) { + if (isInvalid(customId)) { + throw new IllegalArgumentException("Provided custom id is invalid!"); + } + var split = customId.split("\\."); + return new CustomId(split[1], split[2]); + } + + /// Constructs a new runtime-independent [CustomId] from the given definition id. + /// + /// @param definitionId the definition id to construct the [CustomId] from + /// @return a new runtime-independent [CustomId] + @NotNull + public static CustomId independent(@NotNull String definitionId) { + return new CustomId(INDEPENDENT_ID, definitionId); + } + + /// Checks if the passed custom id *doesn't* conform to the defined format of jda-commands. + /// + /// @return `true` if the passed custom id *doesn't* conform to the jda-commands format + public static boolean isInvalid(@NotNull String customId) { + return !(customId.matches(BOUND_CUSTOM_ID_REGEX) || customId.matches(INDEPENDENT_CUSTOM_ID_REGEX)); + } + + /// The String representation of this custom id. + @NotNull + public String id() { + return "%s.%s.%s".formatted(PREFIX, runtimeId, definitionId); + } + + /// Gets the runtime id of this custom id. + /// + /// @return the runtime id + /// @throws IllegalStateException if this custom id is runtime-independent + @NotNull + public String runtimeId() { + if (isIndependent()) { + throw new IllegalStateException("Provided custom id is runtime-independent!"); + } + return runtimeId; + } + + /// Checks if the passed custom id is runtime-independent. + /// + /// @return `true` if the custom id is runtime-independent + public boolean isIndependent() { + return runtimeId.equals(INDEPENDENT_ID); + } + + /// Checks if the passed custom id is runtime-bound. + /// + /// @return `true` if the custom id is runtime-bound + public boolean isBound() { + return !isIndependent(); + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/InteractionDefinition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/InteractionDefinition.java new file mode 100644 index 000000000..4ac5c32a8 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/InteractionDefinition.java @@ -0,0 +1,93 @@ +package com.github.kaktushose.jda.commands.definitions.interactions; + +import com.github.kaktushose.jda.commands.annotations.interactions.Interaction; +import com.github.kaktushose.jda.commands.definitions.Definition; +import com.github.kaktushose.jda.commands.definitions.features.internal.Invokable; +import com.github.kaktushose.jda.commands.definitions.interactions.command.CommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.ContextCommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.SlashCommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.ButtonDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.ComponentDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.EntitySelectMenuDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.StringSelectMenuDefinition; +import com.github.kaktushose.jda.commands.dispatching.middleware.impl.PermissionsMiddleware; +import com.github.kaktushose.jda.commands.dispatching.reply.GlobalReplyConfig; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; + +/// Common interface for all definitions that represent an interaction. +/// +/// @see AutoCompleteDefinition +/// @see ModalDefinition +/// @see ContextCommandDefinition +/// @see SlashCommandDefinition +/// @see ButtonDefinition +/// @see EntitySelectMenuDefinition +/// @see StringSelectMenuDefinition +public sealed interface InteractionDefinition extends Definition, Invokable + permits AutoCompleteDefinition, ModalDefinition, CommandDefinition, ComponentDefinition { + + /// The id for this definition. For interaction definition this is the hash code of the full class name and method + /// name combined. + @NotNull + @Override + default String definitionId() { + return String.valueOf((clazzDescription().clazz().getName() + methodDescription().name()).hashCode()); + } + + /// Creates a new instance of the [Interaction] class. + /// + /// @return a new instance of the declaring class of the method this definition is bound to. + /// @throws InvocationTargetException if the object creation fails + /// @throws InstantiationException if the object creation fails + /// @throws IllegalAccessException if the object creation fails + @ApiStatus.Internal + default Object newInstance() throws InvocationTargetException, InstantiationException, IllegalAccessException { + return clazzDescription().clazz().getConstructors()[0].newInstance(); + } + + /// A possibly-empty [Collection] of permissions for this interaction. + /// + /// @apiNote The [PermissionsMiddleware] will validate the provided permissions. + @NotNull + Collection permissions(); + + /// The [ReplyConfig] that should be used when sending replies. + /// + /// @implNote This will first attempt to use the [`ReplyConfig`][com.github.kaktushose.jda.commands.annotations.interactions.ReplyConfig] + /// annotation of the method and then of the class. If neither is present will fall back to the [GlobalReplyConfig]. + @NotNull + default ReplyConfig replyConfig() { + var global = clazzDescription().annotation(com.github.kaktushose.jda.commands.annotations.interactions.ReplyConfig.class); + var local = methodDescription().annotation(com.github.kaktushose.jda.commands.annotations.interactions.ReplyConfig.class); + + if (global.isEmpty() && local.isEmpty()) { + return new ReplyConfig(); + } + + return local.map(ReplyConfig::new).orElseGet(() -> new ReplyConfig(global.get())); + + } + + /// Stores the configuration values for sending replies. This acts as a representation of + /// [`ReplyConfig`][com.github.kaktushose.jda.commands.annotations.interactions.ReplyConfig]. + /// + /// @see com.github.kaktushose.jda.commands.annotations.interactions.ReplyConfig ReplyConfig + record ReplyConfig(boolean ephemeral, boolean keepComponents, boolean editReply) { + + /// Constructs a new ReplyConfig using the default values specified by [GlobalReplyConfig]. + public ReplyConfig() { + this(GlobalReplyConfig.ephemeral, GlobalReplyConfig.keepComponents, GlobalReplyConfig.editReply); + } + + /// Constructs a new ReplyConfig. + /// + /// @param replyConfig the [`ReplyConfig`][com.github.kaktushose.jda.commands.annotations.interactions.ReplyConfig] to represent + public ReplyConfig(@NotNull com.github.kaktushose.jda.commands.annotations.interactions.ReplyConfig replyConfig) { + this(replyConfig.ephemeral(), replyConfig.keepComponents(), replyConfig.editReply()); + } + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/InteractionRegistry.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/InteractionRegistry.java new file mode 100644 index 000000000..67bce5390 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/InteractionRegistry.java @@ -0,0 +1,203 @@ +package com.github.kaktushose.jda.commands.definitions.interactions; + +import com.github.kaktushose.jda.commands.annotations.Inject; +import com.github.kaktushose.jda.commands.annotations.interactions.*; +import com.github.kaktushose.jda.commands.definitions.Definition; +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.Descriptor; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.definitions.interactions.command.ContextCommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.command.SlashCommandDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.ButtonDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.EntitySelectMenuDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.StringSelectMenuDefinition; +import com.github.kaktushose.jda.commands.dependency.DependencyInjector; +import com.github.kaktushose.jda.commands.dispatching.validation.ValidatorRegistry; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.*; +import java.util.function.Predicate; + +import static com.github.kaktushose.jda.commands.definitions.interactions.command.SlashCommandDefinition.CooldownDefinition; + +/// Central registry for all [InteractionDefinition]s. +public record InteractionRegistry(DependencyInjector dependencyInjector, + ValidatorRegistry validatorRegistry, + LocalizationFunction localizationFunction, + Descriptor descriptor, + Set definitions +) { + + private static final Logger log = LoggerFactory.getLogger(InteractionRegistry.class); + + /// Constructs a new [InteractionRegistry] + /// + /// @param injector the [DependencyInjector] to use + /// @param registry the corresponding [ValidatorRegistry] + /// @param function the [LocalizationFunction] to use + /// @param descriptor the [Descriptor] to use + public InteractionRegistry(DependencyInjector injector, ValidatorRegistry registry, LocalizationFunction function, Descriptor descriptor) { + this(injector, registry, function, descriptor, new HashSet<>()); + } + + /// Scans all given classes and registers the interactions defined in them. + /// + /// @param classes the [Class]es to build the interactions from + public void index(Iterable> classes) { + int oldSize = definitions.size(); + + int count = 0; + for (Class clazz : classes) { + log.debug("Found interaction controller {}", clazz.getName()); + definitions.addAll(indexInteractionClass(descriptor.apply(clazz))); + count++; + } + + log.debug("Successfully registered {} interaction controller(s) with a total of {} interaction(s)!", + count, + definitions.size() - oldSize); + } + + private Collection indexInteractionClass(ClassDescription clazz) { + var interaction = clazz.annotation(Interaction.class).orElseThrow(); + + var fields = Arrays.stream(clazz.clazz().getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Inject.class)) + .toList(); + dependencyInjector.registerDependencies(clazz.clazz(), fields); + + final Set permissions = clazz.annotation(Permissions.class).map(value -> Set.of(value.value())).orElseGet(Set::of); + // get controller level cooldown and use it if no command level cooldown is present + CooldownDefinition cooldown = clazz.annotation(Cooldown.class).map(CooldownDefinition::build).orElse(null); + + var autoCompletes = autoCompleteDefinitions(clazz); + + // index interactions + var interactionDefinitions = interactionDefinitions( + clazz, + validatorRegistry, + localizationFunction, + interaction, + permissions, + cooldown, + autoCompletes + ); + + // validate auto completes + var commandDefinitions = interactionDefinitions.stream() + .filter(SlashCommandDefinition.class::isInstance) + .map(SlashCommandDefinition.class::cast) + .toList(); + + autoCompletes.stream() + .map(AutoCompleteDefinition::commands) + .flatMap(Collection::stream) + .filter(name -> commandDefinitions.stream().noneMatch(command -> command.name().startsWith(name))) + .forEach(s -> log.warn("No Command found for auto complete {}", s)); + + return interactionDefinitions; + } + + private Collection autoCompleteDefinitions(ClassDescription clazz) { + return clazz.methods().stream() + .filter(it -> it.annotation(AutoComplete.class).isPresent()) + .map(method -> AutoCompleteDefinition.build(clazz, method)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private Set interactionDefinitions(ClassDescription clazz, + ValidatorRegistry validatorRegistry, + LocalizationFunction localizationFunction, + Interaction interaction, + Set permissions, + CooldownDefinition cooldown, + Collection autocompletes) { + Set definitions = new HashSet<>(autocompletes); + for (MethodDescription method : clazz.methods()) { + final MethodBuildContext context = new MethodBuildContext( + validatorRegistry, + localizationFunction, + interaction, + permissions, + cooldown, + clazz, + method, + autocompletes + ); + + Optional definition = Optional.empty(); + // index commands + if (method.annotation(SlashCommand.class).isPresent()) { + definition = SlashCommandDefinition.build(context); + } + if (method.annotation(ContextCommand.class).isPresent()) { + definition = ContextCommandDefinition.build(context); + } + + // index components + if (method.annotation(Button.class).isPresent()) { + definition = ButtonDefinition.build(context); + } + if (method.annotation(EntitySelectMenu.class).isPresent()) { + definition = EntitySelectMenuDefinition.build(context); + } + if (method.annotation(StringSelectMenu.class).isPresent()) { + definition = StringSelectMenuDefinition.build(context); + } + + //index modals + if (method.annotation(Modal.class).isPresent()) { + definition = ModalDefinition.build(context); + } + + definition.ifPresent(definitions::add); + } + return definitions; + } + + /// Attempts to find a [Definition] of type [T] based on the given [Predicate]. + /// + /// @param type the type of the [Definition] to find + /// @param internalError `true` if the [Definition] must be found and not finding it + /// indicates a framework bug + /// @param predicate the [Predicate] used to find the [Definition] + /// @param a subtype of [Definition] + /// @return [T] + /// @throws IllegalStateException if no [Definition] was found, although this mandatory should have been the case. + /// This is a rare occasion and can be considered a framework bug + /// @throws IllegalArgumentException if no [Definition] was found, because the [Predicate] didn't include any elements + public T find(Class type, boolean internalError, Predicate predicate) { + return definitions.stream() + .filter(type::isInstance) + .map(type::cast) + .filter(predicate) + .findFirst() + .orElseThrow(() -> internalError + ? new IllegalStateException("No interaction found! Please report this error the the devs of jda-commands.") + : new IllegalArgumentException("No interaction found! Please check that the referenced interaction method exists.") + ); + } + + /// Attempts to find all [Definition]s of type [T] based on the given [Predicate]. + /// + /// @param type the type of the [Definition] to find + /// indicates a framework bug + /// @param predicate the [Predicate] used to find the [Definition]s + /// @param a subtype of [Definition] + /// @return a possibly-empty [Collection] of all [Definition]s that match the given [Predicate] + /// @throws IllegalStateException if no [Definition] was found, although this mandatory should have been the case. + /// This is a rare occasion and can be considered a framework bug + /// @throws IllegalArgumentException if no [Definition] was found, because the [Predicate] didn't include any elements + public Collection find(Class type, Predicate predicate) { + return definitions.stream() + .filter(type::isInstance) + .map(type::cast) + .filter(predicate) + .toList(); + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/MethodBuildContext.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/MethodBuildContext.java new file mode 100644 index 000000000..5f7c0717f --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/MethodBuildContext.java @@ -0,0 +1,27 @@ +package com.github.kaktushose.jda.commands.definitions.interactions; + +import com.github.kaktushose.jda.commands.annotations.interactions.Interaction; +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.definitions.interactions.command.SlashCommandDefinition.CooldownDefinition; +import com.github.kaktushose.jda.commands.dispatching.validation.ValidatorRegistry; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Set; + +/// Holds all objects needed to create an [InteractionDefinition]. +@ApiStatus.Internal +public record MethodBuildContext( + @NotNull ValidatorRegistry validatorRegistry, + @NotNull LocalizationFunction localizationFunction, + Interaction interaction, + @NotNull Set permissions, + @Nullable CooldownDefinition cooldownDefinition, + @NotNull ClassDescription clazz, + @NotNull MethodDescription method, + @NotNull Collection autoCompleteDefinitions +) {} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/ModalDefinition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/ModalDefinition.java new file mode 100644 index 000000000..dc95bc45c --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/ModalDefinition.java @@ -0,0 +1,168 @@ +package com.github.kaktushose.jda.commands.definitions.interactions; + +import com.github.kaktushose.jda.commands.definitions.Definition; +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.definitions.description.ParameterDescription; +import com.github.kaktushose.jda.commands.definitions.features.CustomIdJDAEntity; +import com.github.kaktushose.jda.commands.definitions.features.JDAEntity; +import com.github.kaktushose.jda.commands.definitions.interactions.command.ParameterDefinition; +import com.github.kaktushose.jda.commands.dispatching.events.interactions.ModalEvent; +import com.github.kaktushose.jda.commands.internal.Helpers; +import net.dv8tion.jda.api.interactions.components.text.TextInput; +import net.dv8tion.jda.api.interactions.components.text.TextInputStyle; +import net.dv8tion.jda.api.interactions.modals.Modal; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/// Representation of a modal. +/// +/// @param clazzDescription the [ClassDescription] of the declaring class of the [#methodDescription()] +/// @param methodDescription the [MethodDescription] of the method this definition is bound to +/// @param permissions a [Collection] of permissions for this modal +/// @param title the title of the modal +/// @param textInputs the [`TextInputs`][TextInputDefinition] of this modal +public record ModalDefinition( + @NotNull ClassDescription clazzDescription, + @NotNull MethodDescription methodDescription, + @NotNull Collection permissions, + @NotNull String title, + @NotNull SequencedCollection textInputs +) implements InteractionDefinition, CustomIdJDAEntity { + + /// Builds a new [ModalDefinition] from the given [MethodBuildContext]. + /// + /// @return an [Optional] holding the [ModalDefinition] + public static Optional build(MethodBuildContext context) { + var method = context.method(); + var modal = method.annotation(com.github.kaktushose.jda.commands.annotations.interactions.Modal.class).orElseThrow(); + + // Modals support up to 5 TextInputs + if (method.parameters().isEmpty() || method.parameters().size() > 6) { + log.error("An error has occurred! Skipping Modal {}.{}:", + method.declaringClass().getName(), + method.name(), + new IllegalArgumentException("Invalid amount of parameters! Modals need between 1 and 5 TextInputs")); + return Optional.empty(); + } + + if (Helpers.isIncorrectParameterType(method, 0, ModalEvent.class)) { + return Optional.empty(); + } + + List textInputs = new ArrayList<>(); + for (int i = 1; i < method.parameters().size(); i++) { + var parameter = List.copyOf(method.parameters()).get(i); + TextInputDefinition.build(parameter).ifPresent(textInputs::add); + } + + if (textInputs.isEmpty()) { + log.error("An error has occurred! Skipping Modal {}.{}:", + method.declaringClass().getName(), + method.name(), + new IllegalArgumentException("Modals need at least one valid TextInput")); + return Optional.empty(); + } + + List> signature = new ArrayList<>(); + signature.add(ModalEvent.class); + textInputs.forEach(_ -> signature.add(String.class)); + if (Helpers.checkSignature(method, signature)) { + return Optional.empty(); + } + + + return Optional.of(new ModalDefinition(context.clazz(), method, Helpers.permissions(context), modal.value(), textInputs)); + } + + /// Transforms this definition to an [Modal] with the given custom id. + /// + /// @return the [Modal] + /// @see CustomId#independent(String) + @NotNull + @Override + public Modal toJDAEntity(@NotNull CustomId customId) { + var modal = Modal.create(customId.id(), title); + + textInputs.forEach(textInput -> modal.addActionRow(textInput.toJDAEntity())); + + return modal.build(); + } + + @NotNull + @Override + public String displayName() { + return title; + } + + /// Representation of a modal text input defined by + /// [`TextInput`][com.github.kaktushose.jda.commands.annotations.interactions.TextInput] + public record TextInputDefinition( + @NotNull String label, + @NotNull String placeholder, + @NotNull String defaultValue, + int minValue, + int maxValue, + @NotNull TextInputStyle style, + boolean required + ) implements JDAEntity, Definition { + + /// Builds a new [TextInputDefinition] from the given [ParameterDefinition] + /// + /// @param parameter the [ParameterDefinition] to build the [TextInputDefinition] from + /// @return the new [TextInputDefinition] + public static Optional build(ParameterDescription parameter) { + var optional = parameter.annotation(com.github.kaktushose.jda.commands.annotations.interactions.TextInput.class); + + if (optional.isEmpty()) { + return Optional.empty(); + } + + // TODO for now we only allow Strings, maybe add type adapting in the future + if (!String.class.isAssignableFrom(parameter.type())) { + return Optional.empty(); + } + var textInput = optional.get(); + + return Optional.of(new TextInputDefinition( + textInput.label().isEmpty() ? parameter.name() : textInput.label(), + textInput.value(), + textInput.defaultValue(), + textInput.minValue(), + textInput.maxValue(), + textInput.style(), + textInput.required() + )); + } + + @Override + public @NotNull String displayName() { + return label; + } + + /// Transforms this definition into a [TextInput]. + /// + /// @return the [TextInput] + @NotNull + @Override + public TextInput toJDAEntity() { + var textInput = TextInput.create(label, label, style).setRequired(required); + + if (minValue != -1) { + textInput.setMinLength(minValue); + } + if (maxValue != -1) { + textInput.setMaxLength(maxValue); + } + if (!placeholder.isBlank()) { + textInput.setPlaceholder(placeholder); + } + if (!defaultValue.isBlank()) { + textInput.setValue(defaultValue); + } + + return textInput.build(); + } + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/CommandDefinition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/CommandDefinition.java new file mode 100644 index 000000000..b92a64da9 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/CommandDefinition.java @@ -0,0 +1,40 @@ +package com.github.kaktushose.jda.commands.definitions.interactions.command; + +import com.github.kaktushose.jda.commands.annotations.interactions.CommandScope; +import com.github.kaktushose.jda.commands.definitions.features.JDAEntity; +import com.github.kaktushose.jda.commands.definitions.interactions.InteractionDefinition; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +/// Common interface for command interaction definitions. +/// +/// @see SlashCommandDefinition +/// @see ContextCommandDefinition +public sealed interface CommandDefinition extends InteractionDefinition, JDAEntity permits ContextCommandDefinition, SlashCommandDefinition { + + /// The name of the command. + @NotNull String name(); + + /// Whether this command can only be executed in guilds. + boolean guildOnly(); + + /// Whether this command is nsfw. + boolean nsfw(); + + /// The [Command.Type] of this command. + @NotNull Command.Type commandType(); + + /// A possibly-empty [Set] of [Permission]s this command will be enabled for. + @NotNull Set enabledPermissions(); + + /// The [CommandScope] of this command. + @NotNull CommandScope scope(); + + /// The [LocalizationFunction] to use for this command. + @NotNull LocalizationFunction localizationFunction(); +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/ContextCommandDefinition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/ContextCommandDefinition.java new file mode 100644 index 000000000..ffb63a009 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/ContextCommandDefinition.java @@ -0,0 +1,108 @@ +package com.github.kaktushose.jda.commands.definitions.interactions.command; + +import com.github.kaktushose.jda.commands.annotations.interactions.ContextCommand; +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.definitions.interactions.MethodBuildContext; +import com.github.kaktushose.jda.commands.dispatching.events.interactions.CommandEvent; +import com.github.kaktushose.jda.commands.internal.Helpers; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +import com.github.kaktushose.jda.commands.annotations.interactions.CommandScope; + +/// Representation of a context command. +/// +/// @param clazzDescription the [ClassDescription] of the declaring class of the [#methodDescription()] +/// @param methodDescription the [MethodDescription] of the method this definition is bound to +/// @param permissions a [Collection] of permissions for this command +/// @param name the name of the command +/// @param commandType the [Command.Type] of this command +/// @param scope the [CommandScope] of this command +/// @param guildOnly whether this command can only be executed in guilds +/// @param nsfw whether this command is nsfw +/// @param enabledPermissions a possibly-empty [Set] of [Permission]s this command will be enabled for +/// @param localizationFunction the [LocalizationFunction] to use for this command +public record ContextCommandDefinition( + @NotNull ClassDescription clazzDescription, + @NotNull MethodDescription methodDescription, + @NotNull Collection permissions, + @NotNull String name, + @NotNull Command.Type commandType, + @NotNull CommandScope scope, + boolean guildOnly, + boolean nsfw, + @NotNull Set enabledPermissions, + @NotNull LocalizationFunction localizationFunction +) implements CommandDefinition { + + /// Builds a new [ContextCommandDefinition] from the given [MethodBuildContext]. + /// + /// @return an [Optional] holding the [ContextCommandDefinition] + @NotNull + public static Optional build(MethodBuildContext context) { + var method = context.method(); + ContextCommand command = method.annotation(ContextCommand.class).orElseThrow(); + + var type = switch (command.type()) { + case USER -> User.class; + case MESSAGE -> Message.class; + default -> null; + }; + if (type == null) { + log.error("Invalid command type for context command! Must either be USER or MESSAGE"); + return Optional.empty(); + } + if (Helpers.checkSignature(method, List.of(CommandEvent.class, type))) { + return Optional.empty(); + } + + Set enabledFor = Arrays.stream(command.enabledFor()).collect(Collectors.toSet()); + if (enabledFor.size() == 1 && enabledFor.contains(Permission.UNKNOWN)) { + enabledFor.clear(); + } + + return Optional.of(new ContextCommandDefinition( + context.clazz(), + method, + Helpers.permissions(context), + command.value(), + command.type(), + command.scope(), + command.isGuildOnly(), + command.isNSFW(), + enabledFor, + context.localizationFunction() + )); + } + + /// Transforms this definition into [CommandData]. + /// + /// @return the [CommandData] + @NotNull + @Override + public CommandData toJDAEntity() { + var command = Commands.context(commandType, name); + command.setGuildOnly(guildOnly) + .setNSFW(nsfw) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(enabledPermissions)) + .setLocalizationFunction(localizationFunction); + return command; + } + + @NotNull + @Override + public String displayName() { + return name; + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/reflect/ParameterDefinition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/ParameterDefinition.java similarity index 54% rename from src/main/java/com/github/kaktushose/jda/commands/reflect/ParameterDefinition.java rename to src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/ParameterDefinition.java index 1e9eee659..0aa4e7953 100644 --- a/src/main/java/com/github/kaktushose/jda/commands/reflect/ParameterDefinition.java +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/ParameterDefinition.java @@ -1,11 +1,13 @@ -package com.github.kaktushose.jda.commands.reflect; +package com.github.kaktushose.jda.commands.definitions.interactions.command; import com.github.kaktushose.jda.commands.annotations.constraints.Constraint; import com.github.kaktushose.jda.commands.annotations.constraints.Max; import com.github.kaktushose.jda.commands.annotations.constraints.Min; import com.github.kaktushose.jda.commands.annotations.interactions.Choices; -import com.github.kaktushose.jda.commands.annotations.interactions.Optional; import com.github.kaktushose.jda.commands.annotations.interactions.Param; +import com.github.kaktushose.jda.commands.definitions.Definition; +import com.github.kaktushose.jda.commands.definitions.description.ParameterDescription; +import com.github.kaktushose.jda.commands.definitions.features.JDAEntity; import com.github.kaktushose.jda.commands.dispatching.validation.Validator; import com.github.kaktushose.jda.commands.dispatching.validation.ValidatorRegistry; import net.dv8tion.jda.api.entities.Member; @@ -16,39 +18,44 @@ import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; -import net.dv8tion.jda.api.interactions.commands.Command.Choice; +import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Parameter; import java.util.*; import static java.util.Map.entry; -/** - * Representation of a command parameter. - * - * @see Constraint - * @see Choices - * @see Param - * @since 2.0.0 - */ +/// Representation of a slash command parameter. +/// +/// @param type the [Class] type of the parameter +/// @param optional whether this parameter is optional +/// @param autoComplete whether this parameter supports autocomplete +/// @param defaultValue the default value of this parameter or `null` +/// @param name the name of the parameter +/// @param description the description of the parameter +/// @param choices a [SequencedCollection] of possible [Command.Choice]s for this parameter +/// @param constraints a [Collection] of [ConstraintDefinition]s of this parameter public record ParameterDefinition( - Class type, - boolean isOptional, - String defaultValue, - boolean isPrimitive, - String name, - String description, - List choices, - List constraints -) { - - private static final Map, Class> TYPE_MAPPINGS = Map.ofEntries( + @NotNull Class type, + boolean optional, + boolean autoComplete, + @Nullable String defaultValue, + @NotNull String name, + @NotNull String description, + @NotNull SequencedCollection choices, + @NotNull Collection constraints +) implements Definition, JDAEntity { + + /// A mapping of primitive types and their wrapper classes. + @ApiStatus.Internal + public static final Map, Class> TYPE_MAPPINGS = Map.ofEntries( entry(byte.class, Byte.class), entry(short.class, Short.class), entry(int.class, Integer.class), @@ -69,7 +76,6 @@ public record ParameterDefinition( entry(Boolean.class, OptionType.BOOLEAN), entry(Character.class, OptionType.STRING), entry(String.class, OptionType.STRING), - entry(String[].class, OptionType.STRING), entry(User.class, OptionType.USER), entry(Member.class, OptionType.USER), entry(GuildChannel.class, OptionType.CHANNEL), @@ -97,27 +103,22 @@ public record ParameterDefinition( )) ); - /** - * Builds a new ParameterDefinition. - * - * @param parameter the {@link Parameter} of the command method - * @param registry an instance of the corresponding {@link ValidatorRegistry} - * @return a new ParameterDefinition - */ + /// Builds a new [ParameterDefinition]. + /// + /// @param parameter the [ParameterDescription] to build the [ParameterDefinition] from + /// @param autoComplete whether the [ParameterDescription] should support autocomplete + /// @param validatorRegistry the corresponding [ValidatorRegistry] + /// @return the [ParameterDefinition] @NotNull - public static ParameterDefinition build(@NotNull Parameter parameter, @NotNull ValidatorRegistry registry) { - if (parameter.isVarArgs()) { - throw new IllegalArgumentException("VarArgs is not supported for parameters!"); - } - - Class parameterType = parameter.getType(); - parameterType = TYPE_MAPPINGS.getOrDefault(parameterType, parameterType); - - // Optional - final boolean isOptional = parameter.isAnnotationPresent(Optional.class); - String defaultValue = ""; - if (isOptional) { - defaultValue = parameter.getAnnotation(Optional.class).value(); + public static ParameterDefinition build(ParameterDescription parameter, + boolean autoComplete, + @NotNull ValidatorRegistry validatorRegistry) { + final var parameterType = TYPE_MAPPINGS.getOrDefault(parameter.type(), parameter.type()); + + var optional = parameter.annotation(com.github.kaktushose.jda.commands.annotations.interactions.Optional.class); + var defaultValue = ""; + if (optional.isPresent()) { + defaultValue = optional.get().value(); } if (defaultValue.isEmpty()) { defaultValue = null; @@ -125,90 +126,75 @@ public static ParameterDefinition build(@NotNull Parameter parameter, @NotNull V // index constraints List constraints = new ArrayList<>(); - for (Annotation annotation : parameter.getAnnotations()) { - Class annotationType = annotation.annotationType(); - if (!annotationType.isAnnotationPresent(Constraint.class)) { - continue; - } - - // annotation object is always different, so we cannot cast it. Thus, we need to get the custom error message via reflection - String message = ""; - try { - Method method = annotationType.getDeclaredMethod("message"); - message = (String) method.invoke(annotation); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ignored) { - } - if (message.isEmpty()) { - message = "Parameter validation failed"; - } - - java.util.Optional optional = registry.get(annotationType, parameterType); - if (optional.isPresent()) { - constraints.add(new ConstraintDefinition(optional.get(), message, annotation)); - } - } + parameter.annotations().stream() + .filter(it -> it.annotationType().isAnnotationPresent(Constraint.class)) + .forEach(it -> { + var validator = validatorRegistry.get(it.annotationType(), parameterType); + validator.ifPresent(value -> constraints.add(ConstraintDefinition.build(value, it))); + }); // Param - String name = parameter.getName(); + String name = parameter.name(); String description = "empty description"; - if (parameter.isAnnotationPresent(Param.class)) { - Param param = parameter.getAnnotation(Param.class); - name = param.name().isEmpty() ? name : param.name(); - description = param.value(); + var param = parameter.annotation(Param.class); + if (param.isPresent()) { + name = param.get().name().isEmpty() ? name : param.get().name(); + description = param.get().value(); } name = name.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(); - List choices = new ArrayList<>(); + List commandChoices = new ArrayList<>(); // Options - if (parameter.isAnnotationPresent(Choices.class)) { - Choices opt = parameter.getAnnotation(Choices.class); - for (String option : opt.value()) { + var choices = parameter.annotation(Choices.class); + if (choices.isPresent()) { + for (String option : choices.get().value()) { String[] parsed = option.split(":", 2); if (parsed.length < 1) { continue; } if (parsed.length < 2) { - choices.add(new Choice(parsed[0], parsed[0])); + commandChoices.add(new Command.Choice(parsed[0], parsed[0])); continue; } - choices.add(new Choice(parsed[0], parsed[1])); + commandChoices.add(new Command.Choice(parsed[0], parsed[1])); } } - - // this value is only used to determine if a default value must be present (primitives cannot be null) - boolean usesPrimitives = TYPE_MAPPINGS.containsKey(parameter.getType()); - return new ParameterDefinition( parameterType, - isOptional, + optional.isPresent(), + autoComplete, defaultValue, - usesPrimitives, name, description, - choices, + commandChoices, constraints ); } - /** - * Transforms this parameter definition to a {@link OptionData}. - * - * @param isAutoComplete whether this {@link OptionData} should support auto complete - * @return the transformed {@link OptionData} - */ - public OptionData toOptionData(boolean isAutoComplete) { + @NotNull + @Override + public String displayName() { + return name; + } + + /// Transforms this definition into [OptionData]. + /// + /// @return the [OptionData] + @NotNull + @Override + public OptionData toJDAEntity() { OptionType optionType = OPTION_TYPE_MAPPINGS.getOrDefault(type, OptionType.STRING); OptionData optionData = new OptionData( optionType, name, description, - !isOptional + !optional ); optionData.addChoices(choices); if (optionType.canSupportChoices() && choices.isEmpty()) { - optionData.setAutoComplete(isAutoComplete); + optionData.setAutoComplete(autoComplete); } constraints.stream().filter(constraint -> @@ -223,4 +209,36 @@ public OptionData toOptionData(boolean isAutoComplete) { return optionData; } + + /// Representation of a parameter constraint defined by a constraint annotation. + /// + /// @param validator the corresponding [Validator] + /// @param message the message to display if the constraint fails + /// @param annotation the corresponding annotation object + public record ConstraintDefinition(Validator validator, String message, Object annotation) implements Definition { + + /// Builds a new [ConstraintDefinition]. + /// + /// @param validator the corresponding [Validator] + /// @param annotation the corresponding annotation object + public static ConstraintDefinition build(@NotNull Validator validator, @NotNull Annotation annotation) { + // annotation object is always different, so we cannot cast it. Thus, we need to get the custom error message via reflection + var message = ""; + try { + Method method = annotation.getClass().getDeclaredMethod("message"); + message = (String) method.invoke(annotation); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException _) { + } + if (message.isEmpty()) { + message = "Parameter validation failed"; + } + return new ConstraintDefinition(validator, message, annotation); + } + + @NotNull + @Override + public String displayName() { + return validator.getClass().getName(); + } + } } diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/SlashCommandDefinition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/SlashCommandDefinition.java new file mode 100644 index 000000000..34a911cec --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/SlashCommandDefinition.java @@ -0,0 +1,192 @@ +package com.github.kaktushose.jda.commands.definitions.interactions.command; + +import com.github.kaktushose.jda.commands.annotations.interactions.CommandScope; +import com.github.kaktushose.jda.commands.annotations.interactions.Cooldown; +import com.github.kaktushose.jda.commands.annotations.interactions.SlashCommand; +import com.github.kaktushose.jda.commands.definitions.Definition; +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.definitions.interactions.AutoCompleteDefinition; +import com.github.kaktushose.jda.commands.definitions.interactions.MethodBuildContext; +import com.github.kaktushose.jda.commands.dispatching.events.interactions.CommandEvent; +import com.github.kaktushose.jda.commands.internal.Helpers; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.localization.LocalizationFunction; +import net.dv8tion.jda.internal.utils.Checks; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +/// Representation of a slash command. +/// +/// @param clazzDescription the [ClassDescription] of the declaring class of the [#methodDescription()] +/// @param methodDescription the [MethodDescription] of the method this definition is bound to +/// @param permissions a [Collection] of permissions for this command +/// @param name the name of the command +/// @param scope the [CommandScope] of this command +/// @param guildOnly whether this command can only be executed in guilds +/// @param nsfw whether this command is nsfw +/// @param enabledPermissions a possibly-empty [Set] of [Permission]s this command will be enabled for +/// @param localizationFunction the [LocalizationFunction] to use for this command +/// @param description the command description +/// @param commandParameters a [SequencedCollection] of [ParameterDefinition]s +/// @param cooldown the corresponding [CooldownDefinition] +/// @param isAutoComplete whether this command supports auto complete +public record SlashCommandDefinition( + @NotNull ClassDescription clazzDescription, + @NotNull MethodDescription methodDescription, + @NotNull Collection permissions, + @NotNull String name, + @NotNull CommandScope scope, + boolean guildOnly, + boolean nsfw, + @NotNull Set enabledPermissions, + @NotNull LocalizationFunction localizationFunction, + @NotNull String description, + @NotNull SequencedCollection commandParameters, + @NotNull CooldownDefinition cooldown, + boolean isAutoComplete +) implements CommandDefinition { + + /// Builds a new [SlashCommandDefinition] from the given [MethodBuildContext]. + /// + /// @return an [Optional] holding the [SlashCommandDefinition] + @NotNull + public static Optional build(MethodBuildContext context) { + var method = context.method(); + var interaction = context.interaction(); + var command = method.annotation(SlashCommand.class).orElseThrow(); + + String name = String.join(" ", interaction.value(), command.value()) + .replaceAll(" +", " ") + .trim(); + + if (name.isEmpty()) { + Checks.notBlank(name, "Command name"); + return Optional.empty(); + } + + boolean autoComplete = context.autoCompleteDefinitions().stream() + .map(AutoCompleteDefinition::commands) + .flatMap(Collection::stream) + .anyMatch(name::startsWith); + + // build parameter definitions + List parameters = method.parameters().stream() + .filter(it -> !(CommandEvent.class.isAssignableFrom(it.type()))) + .map(parameter -> ParameterDefinition.build(parameter, autoComplete, context.validatorRegistry())) + .toList(); + + Set enabledFor = Set.of(command.enabledFor()); + if (enabledFor.size() == 1 && enabledFor.contains(Permission.UNKNOWN)) { + enabledFor = Set.of(); + } + + List> signature = new ArrayList<>(); + signature.add(CommandEvent.class); + parameters.forEach(it -> signature.add(it.type())); + if (Helpers.checkSignature(method, signature)) { + return Optional.empty(); + } + + CooldownDefinition cooldownDefinition = CooldownDefinition.build(method.annotation(Cooldown.class).orElse(null)); + if (cooldownDefinition.delay() == 0 && context.cooldownDefinition() != null) { + cooldownDefinition = context.cooldownDefinition(); + } + + return Optional.of(new SlashCommandDefinition( + context.clazz(), + method, + Helpers.permissions(context), + name, + command.scope(), + command.isGuildOnly(), + command.isNSFW(), + enabledFor, + context.localizationFunction(), + command.desc(), + parameters, + cooldownDefinition, + autoComplete + )); + } + + /// Transforms this definition into [SlashCommandData]. + /// + /// @return the [SlashCommandData] + @NotNull + @Override + public SlashCommandData toJDAEntity() { + SlashCommandData command = Commands.slash( + name, + description.replaceAll("N/A", "no description") + ); + command.setGuildOnly(guildOnly) + .setNSFW(nsfw) + .setLocalizationFunction(localizationFunction) + .setDefaultPermissions(DefaultMemberPermissions.enabledFor(enabledPermissions)); + commandParameters.forEach(parameter -> { + if (CommandEvent.class.isAssignableFrom(parameter.type())) { + return; + } + command.addOptions(parameter.toJDAEntity()); + }); + return command; + } + + /// Transforms this definition into [SubcommandData]. + /// + /// @return the [SubcommandData] + public SubcommandData toSubCommandData(String label) { + SubcommandData command = new SubcommandData( + label, + description.replaceAll("N/A", "no description") + + ); + commandParameters.forEach(parameter -> { + command.addOptions(parameter.toJDAEntity()); + }); + return command; + } + + @NotNull + @Override + public String displayName() { + return "/%s".formatted(name); + } + + @NotNull + @Override + public Command.Type commandType() { + return Command.Type.SLASH; + } + + /// Representation of a cooldown definition defined by [Cooldown]. + /// + /// @param delay the delay of the cooldown + /// @param timeUnit the [TimeUnit] of the specified delay + public record CooldownDefinition(long delay, TimeUnit timeUnit) implements Definition { + + /// Builds a new [CooldownDefinition] from the given [Cooldown] annotation. + @NotNull + public static CooldownDefinition build(@Nullable Cooldown cooldown) { + if (cooldown == null) { + return new CooldownDefinition(0, TimeUnit.MILLISECONDS); + } + return new CooldownDefinition(cooldown.value(), cooldown.timeUnit()); + } + + @NotNull + @Override + public String displayName() { + return "Cooldown of %d %s".formatted(delay, timeUnit.name()); + } + } +} diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/package-info.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/package-info.java new file mode 100644 index 000000000..ccc62de37 --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/command/package-info.java @@ -0,0 +1,2 @@ +/// [`Definitions`][com.github.kaktushose.jda.commands.definitions.Definition] that represent a command interaction. +package com.github.kaktushose.jda.commands.definitions.interactions.command; diff --git a/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/component/ButtonDefinition.java b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/component/ButtonDefinition.java new file mode 100644 index 000000000..54bdd943f --- /dev/null +++ b/src/main/java/com/github/kaktushose/jda/commands/definitions/interactions/component/ButtonDefinition.java @@ -0,0 +1,100 @@ +package com.github.kaktushose.jda.commands.definitions.interactions.component; + +import com.github.kaktushose.jda.commands.definitions.description.ClassDescription; +import com.github.kaktushose.jda.commands.definitions.description.MethodDescription; +import com.github.kaktushose.jda.commands.definitions.interactions.CustomId; +import com.github.kaktushose.jda.commands.definitions.interactions.MethodBuildContext; +import com.github.kaktushose.jda.commands.definitions.interactions.component.menu.StringSelectMenuDefinition; +import com.github.kaktushose.jda.commands.dispatching.events.interactions.ComponentEvent; +import com.github.kaktushose.jda.commands.internal.Helpers; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/// Representation of a button. +/// +/// @param clazzDescription the [ClassDescription] of the declaring class of the [#methodDescription()] +/// @param methodDescription the [MethodDescription] of the method this definition is bound to +/// @param permissions a [Collection] of permissions for this button +/// @param label the label of this button +/// @param emoji the [Emoji] of this button or `null` +/// @param link the link of this button or `null` +/// @param style the [ButtonStyle] of this button +public record ButtonDefinition( + @NotNull ClassDescription clazzDescription, + @NotNull MethodDescription methodDescription, + @NotNull Collection permissions, + @NotNull String label, + @Nullable Emoji emoji, + @Nullable String link, + @NotNull ButtonStyle style +) implements ComponentDefinition