Revxrsal / Lamp

A powerful, extendable, flexible yet simple to use commands annotation framework.
MIT License
171 stars 33 forks source link

JDA Slash commands support. Backward compatible #62

Closed bivashy closed 1 year ago

bivashy commented 1 year ago

What is this change supposed to do?

This change adds support of JDA slash commands. And solved several problems with Lamp and Discord slash commands compatibility. Also this change should be backward compatible. This pull request adds:

  1. Registering commands as slash commands in discord. (With handling conflicts).
  2. SuggestionProvider will also handle JDA event CommandAutoCompleteInteractionEvent
  3. Flexible ways to define parameters in slash commands. Manually describe parameter using @OptionData annotation, otherwise Lamp will handle automatically.

Backward compatibility

  1. I've changed JDAActor by deprecating several methods: getMessage(), getEvent().
  2. I didn't removed any value resolver, context resolver, or something like that.
  3. I've added getGenericEvent() method. Also i've created actor package in root directory (/src/main/java/revxrsal/commands/jda/actor) where we define MessageJDAActor, SlashCommandJDAActor that represents actors of specific events. But i didn't move JDAActor

So I think there shouldn't be any problem for migrating to new slash command support. Also i've mentioned alternative methods for Deprecated methods

How to use?

Register slash commands:

public class SomeCommandRegistry {
    private static final String TOKEN = "randomDiscordToken";
    private final JDACommandHandler commandHandler = JDACommandHandler.create(JDABuilder.createDefault(TOKEN).build(), "");

    public void registerCommands() {
        commandHandler.register(new PermissionCommand());
        commandHandler.register(new RoleCommand());
        // Register all existing commands as slash commands in jda
        commandHandler.registerSlashCommands();
    }
}

Manually describe parameter

@Command("parametertest")
public class TestCommand {
    @DefaultFor("parametertest")
    public void test(CommandActor actor,
                     @OptionData(value = OptionType.STRING, name = "animal", description = "What animal you want to test?", choices = {@OptionChoice(name =
                             "dog", value = "DOG"), @OptionChoice(name = "cat", value = "CAT")}) String animal) {
        actor.reply("You have chosen " + animal);
    }
}

Screenshots: animal_manual_autocomplete_example animal_manual_execution_example


If we don't want to manually write parameter, we can do like this: Automatic parameter description

    @DefaultFor("parametertest")
    public void test(CommandActor actor, @Named("animal") Animal animal) {
        actor.reply("You have chosen " + animal);
    }

    public enum Animal {
        DOG, CAT
    }

Screenshots: animal_auto_autocomplete_example animal_manual_execution_example



Also we can use other parameters too, we can define complex commands like this:

    @DefaultFor("complextest")
    @Description("Some complex command :).")
    public void fooCommand(CommandActor actor, @Named("firstinteger") int integer, @Named("secondnumber") double number, @Named("text") String text,
                           @Named("animal") TestEnum animal, @Flag("flagvalue") String flagValue, @Switch("toggle") boolean toggle) {
        actor.reply("Integer is " + integer + ", number is " + number + " text is " + text + " and animal " + animal + ". Flag " + flagValue + ", toggled: " +
                toggle);
    }

Screenshots: Autocomplete: autocomplete_complex_example

Proper typing: integer_type_example

When we execute command: full_input_example

Switch parameter: switch_parameter_example switch_parameter_execution

Note: We can remove @Named annotation, but it will result naming our parameters: arg0, arg1, arg2...

Limitations

If we read documentation of discord slash commands, we can see that we cannot define long chain of subcommands, cannot have base command and subcommand at the same time according to: base_subcommand_conflict

Note: Every edge case was handled in /core/SlashCommandConverter.

And we can see how this pull request handles such edge cases:

@Command("permission")
public class PermissionCommand {
    @DefaultFor("permission")
    public void baseCommandAction() {
        // Do something in base command action
    }

    @Subcommand("add")
    public void permissionAdd(){
        // Add permission
    }

    @Subcommand("remove")
    public void permissionRemove(){
        // Remove permission
    }
}

Expected exception

Exception in thread "main" java.lang.IllegalArgumentException: Paths `permission add` and `permission` are conflicting, remove or modify one of paths
    at revxrsal.commands.jda.core.SlashCommandConverter.validateCommandPaths(SlashCommandConverter.java:117)
    at revxrsal.commands.jda.core.SlashCommandConverter.convertCommands(SlashCommandConverter.java:36)
    at revxrsal.commands.jda.core.JDAHandler.registerSlashCommands(JDAHandler.java:118)

This is considered invalid because permission have at the same time subcommand (add and remove), and command implementation (@DefaultFor("permission"))


Another similar limitation

@Command("permission")
public class PermissionCommand {
    @Subcommand("add")
    public void permissionAdd(){
        // Add permission
    }

    @Subcommand("add hide")
    public void permissionAddHidden() {
        // Add hidden permission
    }

    @Subcommand("remove")
    public void permissionRemove() {
        // Remove permission
    }
}

Expected exception:

Exception in thread "main" java.lang.IllegalArgumentException: Paths `permission add hide` and `permission add` are conflicting, remove or modify one of paths
    at revxrsal.commands.jda.core.SlashCommandConverter.validateCommandPaths(SlashCommandConverter.java:117)
    at revxrsal.commands.jda.core.SlashCommandConverter.convertCommands(SlashCommandConverter.java:36)
    at revxrsal.commands.jda.core.JDAHandler.registerSlashCommands(JDAHandler.java:118)

Where we have two paths permission add and permission add hide. Where permission add is base path, and permission add hide is subcommand of base path. And base path have 'implementation' and 'subcommand' at the same time, which doesn't allowed.


Another problem is, we cannot have too deep subcommands. For example it is impossible create such command path in discord: /permission firstsubcommand secondsubcommand thirdsubcommand Because we can only create subcommand groups, and subcommands.


Related issue: https://github.com/Revxrsal/Lamp/issues/50


If you have any questions, feel free to ask :)

Revxrsal commented 1 year ago

Excellent work! I have a few questions though, as I haven't got a chance to fully explore the code

I'll merge the PR in a separate branch, in case it needs any more modifications or cleanups before moving it upstream. Once again, appreciate the effort!

bivashy commented 1 year ago

Thank you, I appreciate your compliments

Answers:

Here is code permalinks:

Optional related: https://github.com/bivashy/Lamp/blob/master/jda/src/main/java/revxrsal/commands/jda/core/BasicSlashCommandMapper.java#L73

Enum related: https://github.com/bivashy/Lamp/blob/master/jda/src/main/java/revxrsal/commands/jda/core/BasicSlashCommandMapper.java#L78-L81