From 936861664f70271721e75bb9d17aba23b120e0ab Mon Sep 17 00:00:00 2001 From: Azzerial Date: Thu, 10 Jun 2021 19:17:38 +0100 Subject: [PATCH 01/42] added: @Slash.Command annotation --- .../azzerial/slash/annotations/Slash.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/annotations/Slash.java diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java new file mode 100644 index 0000000..a93ba0f --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.annotations; + +import java.lang.annotation.*; + +/** + * The annotation holding the main annotations of the Slash Commands library. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({}) +public @interface Slash { + + /** + * This annotation labels a class as a Slash Command. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface Command { + + /** The name of the Slash Command, must match {@code [a-z0-9-]{1,32}}.*/ + String name(); + /** The description of the Slash Command, cannot be empty or longer than {@code 100} characters.*/ + String description(); + /** The default permission of the Slash Command, whether the command is enabled by default when the app is added to a guild. */ + boolean enabled() default true; + } +} From 8677a17f69e915f4a5f4ddcc75afadbaecee275e Mon Sep 17 00:00:00 2001 From: Azzerial Date: Thu, 10 Jun 2021 19:26:33 +0100 Subject: [PATCH 02/42] added: @Option annotion --- .../azzerial/slash/annotations/Option.java | 35 +++++++++++++++++++ .../azzerial/slash/annotations/Slash.java | 2 ++ 2 files changed, 37 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/annotations/Option.java diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Option.java b/api/src/main/java/com/github/azzerial/slash/annotations/Option.java new file mode 100644 index 0000000..d405ed2 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Option.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.annotations; + +import java.lang.annotation.*; + +/** + * This annotation represents a Slash Command option. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface Option { + + /** The name of the option, must match {@code [a-z0-9-]{1,32}}.*/ + String name(); + /** The description of the option, cannot be empty or longer than {@code 100} characters.*/ + String description(); + /** The requirement of this option, whether the option is required or optional. */ + boolean required() default false; +} diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java index a93ba0f..18e3358 100644 --- a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java @@ -38,6 +38,8 @@ String name(); /** The description of the Slash Command, cannot be empty or longer than {@code 100} characters.*/ String description(); + /** The option list of the Slash Command. */ + Option[] options() default {}; /** The default permission of the Slash Command, whether the command is enabled by default when the app is added to a guild. */ boolean enabled() default true; } From 0c2ef94e6713b4c236e523a7f9852f3c6f0047a7 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Thu, 10 Jun 2021 19:32:22 +0100 Subject: [PATCH 03/42] added: @Choice annotation --- .../azzerial/slash/annotations/Choice.java | 33 +++++++++++++++++++ .../azzerial/slash/annotations/Option.java | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/annotations/Choice.java diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Choice.java b/api/src/main/java/com/github/azzerial/slash/annotations/Choice.java new file mode 100644 index 0000000..60b9d4b --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Choice.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.annotations; + +import java.lang.annotation.*; + +/** + * This annotation represents a choice of a Slash Command option. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface Choice { + + /** The name of the choice, cannot be empty or longer than {@code 100} characters.*/ + String name(); + /** The value of the choice, either an {@code int} or a {@code String}. In the case the value is a {@code String}, it cannot be empty or longer than {@code 100} characters.*/ + String value(); +} diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Option.java b/api/src/main/java/com/github/azzerial/slash/annotations/Option.java index d405ed2..977aa2c 100644 --- a/api/src/main/java/com/github/azzerial/slash/annotations/Option.java +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Option.java @@ -32,4 +32,6 @@ String description(); /** The requirement of this option, whether the option is required or optional. */ boolean required() default false; + /** The choices list of the option. */ + Choice[] choices() default {}; } From 1ac6611e53b42dcce84869d6abc5189649f67a2c Mon Sep 17 00:00:00 2001 From: Azzerial Date: Thu, 10 Jun 2021 19:35:50 +0100 Subject: [PATCH 04/42] added: OptionType enum --- .../azzerial/slash/annotations/Option.java | 2 ++ .../slash/annotations/OptionType.java | 30 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/annotations/OptionType.java diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Option.java b/api/src/main/java/com/github/azzerial/slash/annotations/Option.java index 977aa2c..ec434d4 100644 --- a/api/src/main/java/com/github/azzerial/slash/annotations/Option.java +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Option.java @@ -30,6 +30,8 @@ String name(); /** The description of the option, cannot be empty or longer than {@code 100} characters.*/ String description(); + /** The type of the option. */ + OptionType type(); /** The requirement of this option, whether the option is required or optional. */ boolean required() default false; /** The choices list of the option. */ diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/OptionType.java b/api/src/main/java/com/github/azzerial/slash/annotations/OptionType.java new file mode 100644 index 0000000..0ccecf4 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/annotations/OptionType.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.annotations; + +/** + * This enum represents the type of a Slash Command option. + */ +public enum OptionType { + STRING, + INTEGER, + BOOLEAN, + USER, + CHANNEL, + ROLE, + MENTIONABLE +} From 1b434e112472d97545955b0c8a146b7c545a02e1 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Thu, 10 Jun 2021 19:38:28 +0100 Subject: [PATCH 05/42] added: @Subcommand annotation --- .../azzerial/slash/annotations/Slash.java | 2 ++ .../slash/annotations/Subcommand.java | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/annotations/Subcommand.java diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java index 18e3358..022fd3f 100644 --- a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java @@ -40,6 +40,8 @@ String description(); /** The option list of the Slash Command. */ Option[] options() default {}; + /** The subcommand list of the Slash Command. */ + Subcommand[] subcommands() default {}; /** The default permission of the Slash Command, whether the command is enabled by default when the app is added to a guild. */ boolean enabled() default true; } diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Subcommand.java b/api/src/main/java/com/github/azzerial/slash/annotations/Subcommand.java new file mode 100644 index 0000000..20f4303 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Subcommand.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.annotations; + +import java.lang.annotation.*; + +/** + * This annotation represents a Slash Command subcommand. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface Subcommand { + + /** The name of the subcommand, must match {@code [a-z0-9-]{1,32}}.*/ + String name(); + /** The description of the subcommand, cannot be empty or longer than {@code 100} characters.*/ + String description(); + /** The option list of the subcommand. */ + Option[] options() default {}; +} From c87efb2870254f3258043404f2569efa49574ae4 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Thu, 10 Jun 2021 19:40:28 +0100 Subject: [PATCH 06/42] added: @SubcommandGroup annotation --- .../azzerial/slash/annotations/Slash.java | 2 ++ .../slash/annotations/SubcommandGroup.java | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/annotations/SubcommandGroup.java diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java index 022fd3f..5356f94 100644 --- a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java @@ -42,6 +42,8 @@ Option[] options() default {}; /** The subcommand list of the Slash Command. */ Subcommand[] subcommands() default {}; + /** The subcommand group list of the Slash Command. */ + SubcommandGroup[] subcommandGroups() default {}; /** The default permission of the Slash Command, whether the command is enabled by default when the app is added to a guild. */ boolean enabled() default true; } diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/SubcommandGroup.java b/api/src/main/java/com/github/azzerial/slash/annotations/SubcommandGroup.java new file mode 100644 index 0000000..bbafc2d --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/annotations/SubcommandGroup.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.annotations; + +import java.lang.annotation.*; + +/** + * This annotation represents a Slash Command subcommand group. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface SubcommandGroup { + + /** The name of the subcommand group, must match {@code [a-z0-9-]{1,32}}.*/ + String name(); + /** The description of the subcommand group, cannot be empty or longer than {@code 100} characters.*/ + String description(); + /** The subcommand list of the subcommand group. */ + Subcommand[] subcommands() default {}; +} From 6b5fb186b54ce24d445f78e856428d71298d3bbb Mon Sep 17 00:00:00 2001 From: Azzerial Date: Thu, 10 Jun 2021 23:55:51 +0100 Subject: [PATCH 07/42] fixed: playground build.gradle .env loading --- playground/build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/playground/build.gradle b/playground/build.gradle index d3ab61b..3bcc915 100644 --- a/playground/build.gradle +++ b/playground/build.gradle @@ -33,8 +33,10 @@ java { tasks.named('run') { if (file('.env').exists()) { file('.env').eachLine { - def (key, value) = it.tokenize('=') - environment(key, value) + if (it.matches('[^#][^=]+=.*')) { + def (key, value) = it.tokenize('=') + environment(key, value) + } } } } From eeae27d6cd5b9bd1840e93b7b2b87df83a3296b9 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 01:55:38 +0100 Subject: [PATCH 08/42] added: SlashClient and SlashClientBuilder classes --- .../github/azzerial/slash/SlashClient.java | 30 +++++++++++++ .../azzerial/slash/SlashClientBuilder.java | 42 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/SlashClient.java create mode 100644 api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClient.java b/api/src/main/java/com/github/azzerial/slash/SlashClient.java new file mode 100644 index 0000000..f5a2755 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/SlashClient.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash; + +import net.dv8tion.jda.api.JDA; + +public final class SlashClient { + + private final JDA jda; + + /* Constructors */ + + SlashClient(JDA jda) { + this.jda = jda; + } +} diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java new file mode 100644 index 0000000..6be5a9e --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.internal.utils.Checks; + +public final class SlashClientBuilder { + + private final JDA jda; + + /* Constructors */ + + private SlashClientBuilder(JDA jda) { + this.jda = jda; + } + + /* Methods */ + + public static SlashClientBuilder create(JDA jda) { + Checks.notNull(jda, "JDA"); + return new SlashClientBuilder(jda); + } + + public SlashClient build() { + return new SlashClient(jda); + } +} From 2810fa92c162c5f3f1c1321a15497aaa250f2e38 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 02:01:55 +0100 Subject: [PATCH 09/42] updated: playground example code in Main class --- .../main/java/com/github/azzerial/slash/playground/Main.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/playground/src/main/java/com/github/azzerial/slash/playground/Main.java b/playground/src/main/java/com/github/azzerial/slash/playground/Main.java index 49fb88e..8f1b515 100644 --- a/playground/src/main/java/com/github/azzerial/slash/playground/Main.java +++ b/playground/src/main/java/com/github/azzerial/slash/playground/Main.java @@ -16,6 +16,8 @@ package com.github.azzerial.slash.playground; +import com.github.azzerial.slash.SlashClient; +import com.github.azzerial.slash.SlashClientBuilder; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.requests.GatewayIntent; @@ -41,6 +43,9 @@ public static void main(String[] args) { .createLight(token, EnumSet.noneOf(GatewayIntent.class)) .build() .awaitReady(); + final SlashClient slash = SlashClientBuilder + .create(jda) + .build(); } catch (LoginException e) { logger.error("The bot token was invalid!"); } catch (InterruptedException e) { From a04729c8a433b0ab3f81178e057a78ab5ab26a5b Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 02:16:52 +0100 Subject: [PATCH 10/42] added: @Slash.Tag annotation --- .../github/azzerial/slash/annotations/Slash.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java index 5356f94..80be6c9 100644 --- a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java @@ -47,4 +47,18 @@ /** The default permission of the Slash Command, whether the command is enabled by default when the app is added to a guild. */ boolean enabled() default true; } + + /** + * This annotations assigns an identification tag to a Slash Command for registration purposes. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface Tag { + + /** + * The value of the tag. + */ + String value(); + } } From f1ee4d4db9e7d6cf5d39b0f78ba51bbb37aa4ef6 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 02:30:45 +0100 Subject: [PATCH 11/42] added: AnnotationCompiler class --- .../slash/internal/AnnotationCompiler.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/internal/AnnotationCompiler.java diff --git a/api/src/main/java/com/github/azzerial/slash/internal/AnnotationCompiler.java b/api/src/main/java/com/github/azzerial/slash/internal/AnnotationCompiler.java new file mode 100644 index 0000000..81d9c92 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/internal/AnnotationCompiler.java @@ -0,0 +1,113 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.internal; + +import com.github.azzerial.slash.annotations.Option; +import com.github.azzerial.slash.annotations.Slash; +import com.github.azzerial.slash.annotations.Subcommand; +import com.github.azzerial.slash.annotations.SubcommandGroup; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +import net.dv8tion.jda.internal.utils.Checks; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public final class AnnotationCompiler { + + /* Methods */ + + public CommandData compileCommand(Slash.Command command) { + final CommandData data = new CommandData(command.name(), command.description()); + + data.setDefaultEnabled(command.enabled()); + if (command.subcommands().length != 0) { + data.addSubcommands( + Arrays.stream(command.subcommands()) + .map(this::compileSubcommand) + .collect(Collectors.toList()) + ); + } + if (command.subcommandGroups().length != 0) { + data.addSubcommandGroups( + Arrays.stream(command.subcommandGroups()) + .map(this::compileSubcommandGroup) + .collect(Collectors.toList()) + ); + } + if (command.options().length != 0) { + data.addOptions( + Arrays.stream(command.options()) + .map(this::compileOption) + .collect(Collectors.toList()) + ); + } + return data; + } + + /* Internal */ + + private OptionData compileOption(Option option) { + Checks.notNull(option, "Option"); + final OptionData data = new OptionData( + OptionType.fromKey(option.type().ordinal() + 3), + option.name(), + option.description(), + option.required() + ); + + if (option.type() == com.github.azzerial.slash.annotations.OptionType.STRING + || option.type() == com.github.azzerial.slash.annotations.OptionType.INTEGER) { + data.addChoices( + Arrays.stream(option.choices()) + .map(choice -> { + switch (option.type()) { + case STRING: return new Command.Choice(choice.name(), choice.value()); + case INTEGER: return new Command.Choice(choice.name(), Integer.parseInt(choice.value())); + default: return null; + } + }) + .collect(Collectors.toList()) + ); + } + return data; + } + + private SubcommandData compileSubcommand(Subcommand subcommand) { + Checks.notNull(subcommand, "Subcommand"); + return new SubcommandData(subcommand.name(), subcommand.description()) + .addOptions( + Arrays.stream(subcommand.options()) + .map(this::compileOption) + .collect(Collectors.toList()) + ); + } + + private SubcommandGroupData compileSubcommandGroup(SubcommandGroup subcommandGroup) { + Checks.notNull(subcommandGroup, "SubcommandGroup"); + return new SubcommandGroupData(subcommandGroup.name(), subcommandGroup.description()) + .addSubcommands( + Arrays.stream(subcommandGroup.subcommands()) + .map(this::compileSubcommand) + .collect(Collectors.toList()) + ); + } +} From 204800761465758768f8fc50f36bdd2d755fd2d1 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 02:31:32 +0100 Subject: [PATCH 12/42] added: SlashCommand class --- .../github/azzerial/slash/SlashCommand.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/SlashCommand.java diff --git a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java new file mode 100644 index 0000000..5c83823 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; + +public final class SlashCommand { + + private final JDA jda; + private final String tag; + private final CommandData data; + + /* Constructors */ + + public SlashCommand(JDA jda, String tag, CommandData data) { + this.jda = jda; + this.tag = tag; + this.data = data; + } + + /* Getters & Setters */ + + public String getTag() { + return tag; + } +} From f2dc6c09c28499e0e509b27672492faecd27b097 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 02:32:46 +0100 Subject: [PATCH 13/42] added: CommandRegistry class --- .../slash/internal/CommandRegistry.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java diff --git a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java new file mode 100644 index 0000000..980224f --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.internal; + +import com.github.azzerial.slash.SlashCommand; +import com.github.azzerial.slash.annotations.Slash; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; + +import java.util.HashMap; +import java.util.Map; + +public final class CommandRegistry { + + private final JDA jda; + private final Map registry = new HashMap<>(); + private final AnnotationCompiler annotationCompiler = new AnnotationCompiler(); + + /* Constructors */ + + public CommandRegistry(JDA jda) { + this.jda = jda; + } + + /* Getters & Setters */ + + public SlashCommand registerCommand(Object o) { + final SlashCommand command = compileCommand(o); + + registry.put(command.getTag(), command); + return command; + } + + /* Internal */ + + private SlashCommand compileCommand(Object o) { + final Class cls = o.getClass(); + final Slash.Tag tag = cls.getAnnotation(Slash.Tag.class); + final Slash.Command command = cls.getAnnotation(Slash.Command.class); + + if (tag == null) { + throw new IllegalArgumentException("Provided " + cls.getSimpleName() + ".class is not annotated with @Slash.Tag!"); + } + if (command == null) { + throw new IllegalArgumentException("Provided " + cls.getSimpleName() + ".class is not annotated with @Slash.Command!"); + } + if (registry.containsKey(tag.value())) { + throw new IllegalArgumentException("Tried to register " + cls.getSimpleName() + ".class, but the '" + tag.value() + "' tag was already in use!"); + } + + final CommandData data = annotationCompiler.compileCommand(command); + return new SlashCommand(jda, tag.value(), data); + } +} From e9349fd00c9cc752516352fa6505e84050dadb7e Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 02:34:18 +0100 Subject: [PATCH 14/42] updated: command registration methods in SlashClientBuilder class --- .../azzerial/slash/SlashClientBuilder.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java index 6be5a9e..fb9d648 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java @@ -16,17 +16,24 @@ package com.github.azzerial.slash; +import com.github.azzerial.slash.internal.CommandRegistry; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.internal.utils.Checks; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + public final class SlashClientBuilder { private final JDA jda; + private final CommandRegistry registry; /* Constructors */ private SlashClientBuilder(JDA jda) { this.jda = jda; + this.registry = new CommandRegistry(jda); } /* Methods */ @@ -36,6 +43,21 @@ public static SlashClientBuilder create(JDA jda) { return new SlashClientBuilder(jda); } + public SlashCommand addCommand(Object command) { + Checks.notNull(command, "Command"); + return registry.registerCommand(command); + } + + public List addCommands(Object... commands) { + Checks.notNull(commands, "Commands"); + final List list = new LinkedList<>(); + + for (Object command : commands) { + list.add(addCommand(command)); + } + return Collections.unmodifiableList(list); + } + public SlashClient build() { return new SlashClient(jda); } From 96b2dc81ba6e23ed6feaab368fc9a0836b37f466 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 02:52:16 +0100 Subject: [PATCH 15/42] updated: implemented JDA command interactions in SlashCommand class --- .../github/azzerial/slash/SlashCommand.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java index 5c83823..65bea33 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java @@ -17,13 +17,27 @@ package com.github.azzerial.slash; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.privileges.CommandPrivilege; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.internal.utils.Checks; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; public final class SlashCommand { + private static final long GLOBAL = -1L; + private final JDA jda; private final String tag; private final CommandData data; + private final Map> instances = new HashMap<>(); /* Constructors */ @@ -38,4 +52,150 @@ public SlashCommand(JDA jda, String tag, CommandData data) { public String getTag() { return tag; } + + /* Methods */ + + public synchronized SlashCommand deleteGlobal() { + if (instances.containsKey(GLOBAL)) { + final AtomicReference command = instances.get(GLOBAL); + + command.get().delete().queue(); + instances.remove(GLOBAL); + } + return this; + } + + public SlashCommand deleteGuild(long id) { + return deleteGuild(jda.getGuildById(id)); + } + + public SlashCommand deleteGuild(String id) { + return deleteGuild(jda.getGuildById(id)); + } + + public synchronized SlashCommand deleteGuild(Guild guild) { + Checks.notNull(guild, "Guild"); + if (instances.containsKey(guild.getIdLong())) { + final AtomicReference command = instances.get(guild.getIdLong()); + + command.get().delete().queue(); + instances.remove(guild.getIdLong()); + } + return this; + } + + public RestAction> retrieveGlobalPrivileges(long id) { + return retrieveGlobalPrivileges(jda.getGuildById(id)); + } + + public RestAction> retrieveGlobalPrivileges(String id) { + return retrieveGlobalPrivileges(jda.getGuildById(id)); + } + + public synchronized RestAction> retrieveGlobalPrivileges(Guild guild) { + Checks.notNull(guild, "Guild"); + return instances.containsKey(GLOBAL) ? + instances.get(GLOBAL).get().retrievePrivileges(guild) : + null; + } + + public RestAction> retrieveGuildPrivileges(long id) { + return retrieveGuildPrivileges(jda.getGuildById(id)); + } + + public RestAction> retrieveGuildPrivileges(String id) { + return retrieveGuildPrivileges(jda.getGuildById(id)); + } + + public synchronized RestAction> retrieveGuildPrivileges(Guild guild) { + Checks.notNull(guild, "Guild"); + return instances.containsKey(guild.getIdLong()) ? + instances.get(guild.getIdLong()).get().retrievePrivileges(guild) : + null; + } + + public RestAction> updateGlobalPrivileges(long id, CommandPrivilege... privileges) { + return updateGlobalPrivileges(jda.getGuildById(id), privileges); + } + + public RestAction> updateGlobalPrivileges(String id, CommandPrivilege... privileges) { + return updateGlobalPrivileges(jda.getGuildById(id), privileges); + } + + public synchronized RestAction> updateGlobalPrivileges(Guild guild, CommandPrivilege... privileges) { + Checks.notNull(guild, "Guild"); + Checks.noneNull(privileges, "CommandPrivileges"); + return instances.containsKey(GLOBAL) ? + instances.get(GLOBAL).get().updatePrivileges(guild, privileges) : + null; + } + + public RestAction> updateGlobalPrivileges(long id, Collection privileges) { + return updateGlobalPrivileges(jda.getGuildById(id), privileges); + } + + public RestAction> updateGlobalPrivileges(String id, Collection privileges) { + return updateGlobalPrivileges(jda.getGuildById(id), privileges); + } + + public synchronized RestAction> updateGlobalPrivileges(Guild guild, Collection privileges) { + Checks.notNull(guild, "Guild"); + Checks.noneNull(privileges, "CommandPrivileges"); + return instances.containsKey(GLOBAL) ? + instances.get(GLOBAL).get().updatePrivileges(guild, privileges) : + null; + } + + public RestAction> updateGuildPrivileges(long id, CommandPrivilege... privileges) { + return updateGuildPrivileges(jda.getGuildById(id), privileges); + } + + public RestAction> updateGuildPrivileges(String id, CommandPrivilege... privileges) { + return updateGuildPrivileges(jda.getGuildById(id), privileges); + } + + public synchronized RestAction> updateGuildPrivileges(Guild guild, CommandPrivilege... privileges) { + Checks.notNull(guild, "Guild"); + Checks.noneNull(privileges, "CommandPrivileges"); + return instances.containsKey(guild.getIdLong()) ? + instances.get(guild.getIdLong()).get().updatePrivileges(guild, privileges) : + null; + } + + public RestAction> updateGuildPrivileges(long id, Collection privileges) { + return updateGuildPrivileges(jda.getGuildById(id), privileges); + } + + public RestAction> updateGuildPrivileges(String id, Collection privileges) { + return updateGuildPrivileges(jda.getGuildById(id), privileges); + } + + public synchronized RestAction> updateGuildPrivileges(Guild guild, Collection privileges) { + Checks.notNull(guild, "Guild"); + Checks.noneNull(privileges, "CommandPrivileges"); + return instances.containsKey(guild.getIdLong()) ? + instances.get(guild.getIdLong()).get().updatePrivileges(guild, privileges) : + null; + } + + public synchronized SlashCommand upsertGlobal() { + jda.upsertCommand(data) + .queue(command -> instances.put(GLOBAL, new AtomicReference<>(command))); + return this; + } + + public SlashCommand upsertGuild(long id) { + return upsertGuild(jda.getGuildById(id)); + } + + public SlashCommand upsertGuild(String id) { + return upsertGuild(jda.getGuildById(id)); + } + + public synchronized SlashCommand upsertGuild(Guild guild) { + Checks.notNull(guild, "Guild"); + guild.upsertCommand(data) + .queue(command -> instances.put(guild.getIdLong(), new AtomicReference<>(command))); + return this; + } } From 2dbe91657d7bb6f1b89f9e02a862b45ba47c12f9 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 02:54:03 +0100 Subject: [PATCH 16/42] updated: command registration return in SlashClientBuilder class --- .../github/azzerial/slash/SlashClientBuilder.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java index fb9d648..d6ff8fc 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java @@ -43,19 +43,18 @@ public static SlashClientBuilder create(JDA jda) { return new SlashClientBuilder(jda); } - public SlashCommand addCommand(Object command) { + public SlashClientBuilder addCommand(Object command) { Checks.notNull(command, "Command"); - return registry.registerCommand(command); + registry.registerCommand(command); + return this; } - public List addCommands(Object... commands) { + public SlashClientBuilder addCommands(Object... commands) { Checks.notNull(commands, "Commands"); - final List list = new LinkedList<>(); - for (Object command : commands) { - list.add(addCommand(command)); + addCommand(command); } - return Collections.unmodifiableList(list); + return this; } public SlashClient build() { From 26042efc095b29b932714d17d9222515d2054f4a Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 03:00:23 +0100 Subject: [PATCH 17/42] updated: implemented a SlashCommand getter in SlashClient class --- .../java/com/github/azzerial/slash/SlashClient.java | 11 ++++++++++- .../com/github/azzerial/slash/SlashClientBuilder.java | 2 +- .../azzerial/slash/internal/CommandRegistry.java | 6 ++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClient.java b/api/src/main/java/com/github/azzerial/slash/SlashClient.java index f5a2755..b07e870 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashClient.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashClient.java @@ -16,15 +16,24 @@ package com.github.azzerial.slash; +import com.github.azzerial.slash.internal.CommandRegistry; import net.dv8tion.jda.api.JDA; public final class SlashClient { private final JDA jda; + private final CommandRegistry registry; /* Constructors */ - SlashClient(JDA jda) { + SlashClient(JDA jda, CommandRegistry registry) { this.jda = jda; + this.registry = registry; + } + + /* Methods */ + + public SlashCommand getCommand(String tag) { + return registry.getCommand(tag); } } diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java index d6ff8fc..3e2f070 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java @@ -58,6 +58,6 @@ public SlashClientBuilder addCommands(Object... commands) { } public SlashClient build() { - return new SlashClient(jda); + return new SlashClient(jda, registry); } } diff --git a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java index 980224f..9778f37 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java @@ -38,6 +38,12 @@ public CommandRegistry(JDA jda) { /* Getters & Setters */ + public SlashCommand getCommand(String tag) { + return registry.get(tag); + } + + /* Methods */ + public SlashCommand registerCommand(Object o) { final SlashCommand command = compileCommand(o); From 70592727a4ce97b94482befdeea634927f91e615 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 03:04:38 +0100 Subject: [PATCH 18/42] updated: added the guild_id field to the .env --- playground/sample.env | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playground/sample.env b/playground/sample.env index 36724e2..8600687 100644 --- a/playground/sample.env +++ b/playground/sample.env @@ -1,4 +1,7 @@ # Save this file as ".env" after editing! # Discord bot token -token= \ No newline at end of file +token= + +# SlashClient development guild id +guild_id= \ No newline at end of file From d5f2bc00bad923d23e056c63eed707376c6de7e1 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 03:06:27 +0100 Subject: [PATCH 19/42] added: PingCommand class --- .../playground/commands/PingCommand.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java diff --git a/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java b/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java new file mode 100644 index 0000000..360c29f --- /dev/null +++ b/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.playground.commands; + +import com.github.azzerial.slash.annotations.Slash; + +@Slash.Tag("ping") +@Slash.Command( + name = "ping", + description = "Check the current latency" +) +public final class PingCommand {} From 52061c9de6695690e50e352ad5299d94170dbc8a Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 03:07:15 +0100 Subject: [PATCH 20/42] updated: added the PingCommand to the playground example --- .../java/com/github/azzerial/slash/playground/Main.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/playground/src/main/java/com/github/azzerial/slash/playground/Main.java b/playground/src/main/java/com/github/azzerial/slash/playground/Main.java index 8f1b515..313a378 100644 --- a/playground/src/main/java/com/github/azzerial/slash/playground/Main.java +++ b/playground/src/main/java/com/github/azzerial/slash/playground/Main.java @@ -18,6 +18,7 @@ import com.github.azzerial.slash.SlashClient; import com.github.azzerial.slash.SlashClientBuilder; +import com.github.azzerial.slash.playground.commands.PingCommand; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.requests.GatewayIntent; @@ -36,6 +37,7 @@ public final class Main { public static void main(String[] args) { // get the .env config variables final String token = System.getenv("token"); + final String guildId = System.getenv("guild_id"); try { // create the JDA instance @@ -43,9 +45,14 @@ public static void main(String[] args) { .createLight(token, EnumSet.noneOf(GatewayIntent.class)) .build() .awaitReady(); + // create the SlashClient instance final SlashClient slash = SlashClientBuilder .create(jda) + .addCommand(new PingCommand()) // register the ping command .build(); + + // add the command to a guild if not already added + slash.getCommand("ping").upsertGuild(guildId); } catch (LoginException e) { logger.error("The bot token was invalid!"); } catch (InterruptedException e) { From 96d7d52602aeb70d7db1a02398316b26ac87113e Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 04:29:36 +0100 Subject: [PATCH 21/42] added: @Slash.Handler annotation --- .../github/azzerial/slash/annotations/Slash.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java index 80be6c9..09827d5 100644 --- a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java @@ -48,6 +48,20 @@ boolean enabled() default true; } + /** + * This annotation labels a method as a Slash Command handler. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @interface Handler { + + /** + * The path to the handler. + */ + String value() default ""; + } + /** * This annotations assigns an identification tag to a Slash Command for registration purposes. */ From 1846ee68c30b2aae3ccf16a94a48fc8addaabc90 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 04:30:21 +0100 Subject: [PATCH 22/42] updated: added method handlers map in SlashCommand class --- .../github/azzerial/slash/SlashCommand.java | 5 +- .../slash/internal/AnnotationCompiler.java | 69 ++++++++++++++++++- .../slash/internal/CommandRegistry.java | 12 ++-- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java index 65bea33..e62e8f3 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java @@ -24,6 +24,7 @@ import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.internal.utils.Checks; +import java.lang.reflect.Method; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -37,14 +38,16 @@ public final class SlashCommand { private final JDA jda; private final String tag; private final CommandData data; + private final Map handlers; private final Map> instances = new HashMap<>(); /* Constructors */ - public SlashCommand(JDA jda, String tag, CommandData data) { + public SlashCommand(JDA jda, String tag, CommandData data, Map handlers) { this.jda = jda; this.tag = tag; this.data = data; + this.handlers = handlers; } /* Getters & Setters */ diff --git a/api/src/main/java/com/github/azzerial/slash/internal/AnnotationCompiler.java b/api/src/main/java/com/github/azzerial/slash/internal/AnnotationCompiler.java index 81d9c92..1d38080 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/AnnotationCompiler.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/AnnotationCompiler.java @@ -20,6 +20,7 @@ import com.github.azzerial.slash.annotations.Slash; import com.github.azzerial.slash.annotations.Subcommand; import com.github.azzerial.slash.annotations.SubcommandGroup; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.CommandData; @@ -28,7 +29,9 @@ import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; import net.dv8tion.jda.internal.utils.Checks; -import java.util.Arrays; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; import java.util.stream.Collectors; public final class AnnotationCompiler { @@ -63,6 +66,26 @@ public CommandData compileCommand(Slash.Command command) { return data; } + public Map compileHandlers(Class cls, CommandData data) { + final List methods = Arrays.stream(cls.getDeclaredMethods()) + .filter(method -> + (method.getModifiers() & (Modifier.PROTECTED | Modifier.PRIVATE)) == 0 + && method.isAnnotationPresent(Slash.Handler.class) + && method.getParameterCount() == 1 + && method.getParameterTypes()[0] == SlashCommandEvent.class + ) + .collect(Collectors.toList()); + final Map> handlers = buildHandlersMap(methods); + + checkHandlers(data, handlers); + return new HashMap() {{ + handlers.forEach((k, v) -> put( + data.getName() + (k.isEmpty() ? "" : "/" + k), + v.get(0) + )); + }}; + } + /* Internal */ private OptionData compileOption(Option option) { @@ -110,4 +133,48 @@ private SubcommandGroupData compileSubcommandGroup(SubcommandGroup subcommandGro .collect(Collectors.toList()) ); } + + private Map> buildHandlersMap(List methods) { + final Map> handlers = new HashMap<>(); + + for (Method method : methods) { + final Slash.Handler handler = method.getAnnotation(Slash.Handler.class); + + if (!handlers.containsKey(handler.value())) { + handlers.put(handler.value(), new LinkedList<>()); + } + handlers.get(handler.value()).add(method); + } + return handlers; + } + + private void checkHandlers(CommandData data, Map> handlers) { + final Set paths = new HashSet<>(); + + if (!data.getSubcommandGroups().isEmpty()) { + for (SubcommandGroupData subcommandGroup : data.getSubcommandGroups()) { + for (SubcommandData subcommand : subcommandGroup.getSubcommands()) { + paths.add(subcommandGroup.getName() + "/" + subcommand.getName()); + } + } + } else if (!data.getSubcommands().isEmpty()) { + for (SubcommandData subcommand : data.getSubcommands()) { + paths.add(subcommand.getName()); + } + } else { + paths.add(""); + } + + for (String path : handlers.keySet()) { + final List methods = handlers.get(path); + + if (!paths.contains(path)) { + throw new IllegalArgumentException("Could not find the '" + path + "' command path in '" + data.getName() + "'!"); + } + if (methods.size() != 1) { + throw new IllegalArgumentException("Multiple handlers were declared for the '" + path + "' command path in '" + data.getName() + "'!"); + } + } + } + } diff --git a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java index 9778f37..c86235a 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java @@ -21,6 +21,7 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; @@ -44,8 +45,8 @@ public SlashCommand getCommand(String tag) { /* Methods */ - public SlashCommand registerCommand(Object o) { - final SlashCommand command = compileCommand(o); + public SlashCommand registerCommand(Object obj) { + final SlashCommand command = compileCommand(obj); registry.put(command.getTag(), command); return command; @@ -53,8 +54,8 @@ public SlashCommand registerCommand(Object o) { /* Internal */ - private SlashCommand compileCommand(Object o) { - final Class cls = o.getClass(); + private SlashCommand compileCommand(Object obj) { + final Class cls = obj.getClass(); final Slash.Tag tag = cls.getAnnotation(Slash.Tag.class); final Slash.Command command = cls.getAnnotation(Slash.Command.class); @@ -69,6 +70,7 @@ private SlashCommand compileCommand(Object o) { } final CommandData data = annotationCompiler.compileCommand(command); - return new SlashCommand(jda, tag.value(), data); + final Map handlers = annotationCompiler.compileHandlers(cls, data); + return new SlashCommand(jda, tag.value(), data, handlers); } } From 9fbe9645575e06186be2d3e5c86e75f5ea581750 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 04:33:13 +0100 Subject: [PATCH 23/42] added: InteractionListener class --- .../github/azzerial/slash/SlashClient.java | 6 ++++ .../slash/internal/InteractionListener.java | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClient.java b/api/src/main/java/com/github/azzerial/slash/SlashClient.java index b07e870..3a4a7cd 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashClient.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashClient.java @@ -17,18 +17,24 @@ package com.github.azzerial.slash; import com.github.azzerial.slash.internal.CommandRegistry; +import com.github.azzerial.slash.internal.InteractionListener; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.hooks.EventListener; public final class SlashClient { private final JDA jda; private final CommandRegistry registry; + private final EventListener listener; /* Constructors */ SlashClient(JDA jda, CommandRegistry registry) { this.jda = jda; this.registry = registry; + this.listener = new InteractionListener(registry); + + jda.addEventListener(listener); } /* Methods */ diff --git a/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java b/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java new file mode 100644 index 0000000..66d7ad9 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.internal; + +import net.dv8tion.jda.api.hooks.ListenerAdapter; + +public final class InteractionListener extends ListenerAdapter { + + private final CommandRegistry registry; + + /* Constructors */ + + public InteractionListener(CommandRegistry registry) { + this.registry = registry; + } +} From 6af9e6231b18c1da5be67d7f9734aaba223bb7d1 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 04:37:47 +0100 Subject: [PATCH 24/42] updated: implemented SlashCommandEvent callback --- .../github/azzerial/slash/SlashCommand.java | 20 +++++++++++++- .../slash/internal/CommandRegistry.java | 9 ++++++- .../slash/internal/InteractionListener.java | 26 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java index e62e8f3..8d54e40 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; public final class SlashCommand { @@ -38,15 +39,17 @@ public final class SlashCommand { private final JDA jda; private final String tag; private final CommandData data; + private final Object obj; private final Map handlers; private final Map> instances = new HashMap<>(); /* Constructors */ - public SlashCommand(JDA jda, String tag, CommandData data, Map handlers) { + public SlashCommand(JDA jda, String tag, CommandData data, Object obj, Map handlers) { this.jda = jda; this.tag = tag; this.data = data; + this.obj = obj; this.handlers = handlers; } @@ -56,6 +59,21 @@ public String getTag() { return tag; } + public Object getObjectInstance() { + return obj; + } + + public List getCommandIds() { + return instances.values().stream() + .map(AtomicReference::get) + .map(Command::getIdLong) + .collect(Collectors.toList()); + } + + public Map getHandlers() { + return handlers; + } + /* Methods */ public synchronized SlashCommand deleteGlobal() { diff --git a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java index c86235a..0a01ce2 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java @@ -43,6 +43,13 @@ public SlashCommand getCommand(String tag) { return registry.get(tag); } + public SlashCommand getCommandById(long id) { + return registry.values().stream() + .filter(command -> command.getCommandIds().contains(id)) + .findFirst() + .orElse(null); + } + /* Methods */ public SlashCommand registerCommand(Object obj) { @@ -71,6 +78,6 @@ private SlashCommand compileCommand(Object obj) { final CommandData data = annotationCompiler.compileCommand(command); final Map handlers = annotationCompiler.compileHandlers(cls, data); - return new SlashCommand(jda, tag.value(), data, handlers); + return new SlashCommand(jda, tag.value(), data, obj, handlers); } } diff --git a/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java b/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java index 66d7ad9..eb57b7f 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java @@ -16,8 +16,13 @@ package com.github.azzerial.slash.internal; +import com.github.azzerial.slash.SlashCommand; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + public final class InteractionListener extends ListenerAdapter { private final CommandRegistry registry; @@ -27,4 +32,25 @@ public final class InteractionListener extends ListenerAdapter { public InteractionListener(CommandRegistry registry) { this.registry = registry; } + + /* Methods */ + + @Override + public void onSlashCommand(SlashCommandEvent event) { + if (event.getUser().isBot()) { + return; + } + + final SlashCommand command = registry.getCommandById(event.getCommandIdLong()); + + if (command != null) { + final Method method = command.getHandlers().get(event.getCommandPath()); + + if (method != null) { + try { + method.invoke(command.getObjectInstance(), event); + } catch (IllegalAccessException | InvocationTargetException ignored) {} + } + } + } } From 1dcf119ce1fa69e42fde9dcaa1ad7913e5dce40c Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 04:54:56 +0100 Subject: [PATCH 25/42] fixed: removed unused imports in SlashClientBuilder class --- .../java/com/github/azzerial/slash/SlashClientBuilder.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java index 3e2f070..af8401d 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java @@ -20,10 +20,6 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.internal.utils.Checks; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - public final class SlashClientBuilder { private final JDA jda; From f62193c316a0ba95dd52b8bcdd8d2418920cf6ce Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 04:55:27 +0100 Subject: [PATCH 26/42] updated: implemented handler callback in PingCommand class --- .../playground/commands/PingCommand.java | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java b/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java index 360c29f..3cbf786 100644 --- a/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java +++ b/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java @@ -17,10 +17,42 @@ package com.github.azzerial.slash.playground.commands; import com.github.azzerial.slash.annotations.Slash; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; @Slash.Tag("ping") @Slash.Command( name = "ping", description = "Check the current latency" ) -public final class PingCommand {} +public final class PingCommand { + + private final MessageEmbed initialMessage = buildPingMessage("..."); + + /* Methods */ + + @Slash.Handler + public void ping(SlashCommandEvent event) { + final long time = System.currentTimeMillis(); + + event.deferReply(true) + .addEmbeds(initialMessage) + .flatMap(v -> { + final long latency = System.currentTimeMillis() - time; + final String ms = Long.toUnsignedString(latency); + final MessageEmbed message = buildPingMessage(ms); + return event.getHook().editOriginalEmbeds(message); + }) + .queue(); + } + + /* Internal */ + + private MessageEmbed buildPingMessage(String ms) { + final EmbedBuilder builder = new EmbedBuilder(); + + builder.setDescription("**Ping:** `" + ms + "`ms"); + return builder.build(); + } +} From 43f96052fa50cbf153398cc83f55c1f2d011ff90 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 05:17:56 +0100 Subject: [PATCH 27/42] updated: implemented command loading on SlashClientBuilder#build --- .../azzerial/slash/SlashClientBuilder.java | 48 +++++++++++++++++++ .../github/azzerial/slash/SlashCommand.java | 10 +++- .../slash/internal/CommandRegistry.java | 5 ++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java index af8401d..7eda7eb 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashClientBuilder.java @@ -18,13 +18,19 @@ import com.github.azzerial.slash.internal.CommandRegistry; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.internal.utils.Checks; +import java.util.Collection; +import java.util.List; + public final class SlashClientBuilder { private final JDA jda; private final CommandRegistry registry; + private boolean deleteUnregisteredCommands = false; + /* Constructors */ private SlashClientBuilder(JDA jda) { @@ -53,7 +59,49 @@ public SlashClientBuilder addCommands(Object... commands) { return this; } + public SlashClientBuilder deleteUnregisteredCommands(boolean enabled) { + this.deleteUnregisteredCommands = enabled; + return this; + } + public SlashClient build() { + final Collection commands = registry.getCommands(); + + loadGlobalCommands(commands); + loadGuildCommands(commands); return new SlashClient(jda, registry); } + + /* Internal */ + + private void loadGlobalCommands(Collection commands) { + final List cmds = jda.retrieveCommands().complete(); + + for (SlashCommand command : commands) { + for (Command cmd : cmds) { + if (cmd.getName().equals(command.getData().getName())) { + command.putCommand(SlashCommand.GLOBAL, cmd); + } else if (deleteUnregisteredCommands) { + cmd.delete().queue(); + } + } + } + } + + private void loadGuildCommands(Collection commands) { + jda.getGuilds() + .forEach(guild -> { + final List cmds = guild.retrieveCommands().complete(); + + for (SlashCommand command : commands) { + for (Command cmd : cmds) { + if (cmd.getName().equals(command.getData().getName())) { + command.putCommand(guild.getIdLong(), cmd); + } else if (deleteUnregisteredCommands) { + cmd.delete().queue(); + } + } + } + }); + } } diff --git a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java index 8d54e40..cde9b25 100644 --- a/api/src/main/java/com/github/azzerial/slash/SlashCommand.java +++ b/api/src/main/java/com/github/azzerial/slash/SlashCommand.java @@ -34,7 +34,7 @@ public final class SlashCommand { - private static final long GLOBAL = -1L; + public static final long GLOBAL = -1L; private final JDA jda; private final String tag; @@ -59,6 +59,10 @@ public String getTag() { return tag; } + public CommandData getData() { + return data; + } + public Object getObjectInstance() { return obj; } @@ -74,6 +78,10 @@ public Map getHandlers() { return handlers; } + public synchronized void putCommand(long id, Command command) { + instances.put(id, new AtomicReference<>(command)); + } + /* Methods */ public synchronized SlashCommand deleteGlobal() { diff --git a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java index 0a01ce2..b38f823 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java @@ -22,6 +22,7 @@ import net.dv8tion.jda.api.interactions.commands.build.CommandData; import java.lang.reflect.Method; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -50,6 +51,10 @@ public SlashCommand getCommandById(long id) { .orElse(null); } + public Collection getCommands() { + return registry.values(); + } + /* Methods */ public SlashCommand registerCommand(Object obj) { From f27b27d351ddf7564e9d6572fd71fb088b5b698d Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 07:55:31 +0100 Subject: [PATCH 28/42] added: @Slash.Button annotation --- .../com/github/azzerial/slash/annotations/Slash.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java index 09827d5..82fa436 100644 --- a/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java +++ b/api/src/main/java/com/github/azzerial/slash/annotations/Slash.java @@ -48,6 +48,18 @@ boolean enabled() default true; } + /** + * This annotation labels a method as a Slash Command button handler. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @interface Button { + + /** The tag of the button. */ + String value(); + } + /** * This annotation labels a method as a Slash Command handler. */ From 9b5ef4a2b4f69de263eabb986bd6f589fdff6322 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 07:58:23 +0100 Subject: [PATCH 29/42] added: ButtonRegistry and ButtonCallback classes --- .../slash/internal/ButtonCallback.java | 42 +++++++++ .../slash/internal/ButtonRegistry.java | 85 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/internal/ButtonCallback.java create mode 100644 api/src/main/java/com/github/azzerial/slash/internal/ButtonRegistry.java diff --git a/api/src/main/java/com/github/azzerial/slash/internal/ButtonCallback.java b/api/src/main/java/com/github/azzerial/slash/internal/ButtonCallback.java new file mode 100644 index 0000000..2e0ef4e --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/internal/ButtonCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.internal; + +import java.lang.reflect.Method; + +public final class ButtonCallback { + + private final Object obj; + private final Method method; + + /* Constructors */ + + ButtonCallback(Object obj, Method method) { + this.obj = obj; + this.method = method; + } + + /* Getters & Setters */ + + public Object getObjectInstance() { + return obj; + } + + public Method getMethod() { + return method; + } +} diff --git a/api/src/main/java/com/github/azzerial/slash/internal/ButtonRegistry.java b/api/src/main/java/com/github/azzerial/slash/internal/ButtonRegistry.java new file mode 100644 index 0000000..39f3a11 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/internal/ButtonRegistry.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.internal; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public final class ButtonRegistry { + + public static final int CODE_LENGTH = 4; + public static final int ID_BASE = 32; + private static final ButtonRegistry INSTANCE = new ButtonRegistry(); + + private final List codes; + private final Map mappings = new HashMap<>(); + + /* Constructors */ + + private ButtonRegistry() { + this.codes = new LinkedList<>(); + + codes.add(null); + } + + /* Getters & Setters */ + + public static ButtonRegistry getInstance() { + return INSTANCE; + } + + public String createButtonId(String tag, String data) { + final int code = codes.indexOf(tag); + return String.format( + "%-" + CODE_LENGTH + "." + CODE_LENGTH + "s" + + "%." + (100 - CODE_LENGTH) + "s", + Integer.toUnsignedString(code == -1 ? 0 : code, ID_BASE), + data == null ? "" : data + ); + } + + public ButtonCallback getButtonCallback(String id) { + final int code = Integer.parseUnsignedInt(parseCode(id), ID_BASE); + final String tag = codes.get(code); + return mappings.get(tag); + } + + /* Methods */ + + public void registerButton(String tag, ButtonCallback callback) { + if (!codes.contains(tag)) { + codes.add(tag); + mappings.put(tag, callback); + } + } + + /* Internal */ + + private String parseCode(String s) { + return s == null || s.isEmpty() ? + null : + s.substring(0, Math.min(CODE_LENGTH, s.length())).trim(); + } + + private String parseData(String s) { + return s == null || s.length() <= CODE_LENGTH ? + null : + s.substring(CODE_LENGTH); + } +} From 16ca3af634a88e03a5f30a54165fe62ca81adfd9 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 07:59:29 +0100 Subject: [PATCH 30/42] updated: added button registration --- .../slash/internal/CommandRegistry.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java index b38f823..191bef4 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/CommandRegistry.java @@ -19,12 +19,12 @@ import com.github.azzerial.slash.SlashCommand; import com.github.azzerial.slash.annotations.Slash; import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; import net.dv8tion.jda.api.interactions.commands.build.CommandData; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.lang.reflect.Modifier; +import java.util.*; public final class CommandRegistry { @@ -60,6 +60,7 @@ public Collection getCommands() { public SlashCommand registerCommand(Object obj) { final SlashCommand command = compileCommand(obj); + registerButtons(obj); registry.put(command.getTag(), command); return command; } @@ -85,4 +86,24 @@ private SlashCommand compileCommand(Object obj) { final Map handlers = annotationCompiler.compileHandlers(cls, data); return new SlashCommand(jda, tag.value(), data, obj, handlers); } + + private void registerButtons(Object obj) { + final Class cls = obj.getClass(); + + Arrays.stream(cls.getDeclaredMethods()) + .filter(method -> + (method.getModifiers() & (Modifier.PROTECTED | Modifier.PRIVATE)) == 0 + && method.isAnnotationPresent(Slash.Button.class) + && method.getParameterCount() == 1 + && method.getParameterTypes()[0] == ButtonClickEvent.class + ) + .sorted(Comparator.comparing(Method::getName)) + .forEach(method -> { + final String tag = method.getAnnotation(Slash.Button.class).value(); + + if (!tag.isEmpty()) { + ButtonRegistry.getInstance().registerButton(tag, new ButtonCallback(obj, method)); + } + }); + } } From 92ced92d27dd49bdeee9cd24b59a48e60ef97fb9 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 08:00:34 +0100 Subject: [PATCH 31/42] updated: implemented ButtonClickEvent callback --- .../slash/internal/InteractionListener.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java b/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java index eb57b7f..8a055a8 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/InteractionListener.java @@ -17,6 +17,7 @@ package com.github.azzerial.slash.internal; import com.github.azzerial.slash.SlashCommand; +import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; @@ -53,4 +54,21 @@ public void onSlashCommand(SlashCommandEvent event) { } } } + + @Override + public void onButtonClick(ButtonClickEvent event) { + if (event.getUser().isBot()) { + return; + } + + final ButtonCallback button = ButtonRegistry.getInstance().getButtonCallback(event.getComponentId()); + + if (button != null) { + final Method method = button.getMethod(); + + try { + method.invoke(button.getObjectInstance(), event); + } catch (IllegalAccessException | InvocationTargetException ignored) {} + } + } } From 56ef0484b642d15894561f876ad108df103bcc8d Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 08:01:06 +0100 Subject: [PATCH 32/42] added: SlashButton class --- .../slash/components/SlashButton.java | 265 ++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/components/SlashButton.java diff --git a/api/src/main/java/com/github/azzerial/slash/components/SlashButton.java b/api/src/main/java/com/github/azzerial/slash/components/SlashButton.java new file mode 100644 index 0000000..75031c6 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/components/SlashButton.java @@ -0,0 +1,265 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.components; + +import com.github.azzerial.slash.internal.ButtonRegistry; +import net.dv8tion.jda.api.entities.Emoji; +import net.dv8tion.jda.api.interactions.components.ButtonStyle; +import net.dv8tion.jda.api.interactions.components.Component; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.interactions.ButtonImpl; +import net.dv8tion.jda.internal.utils.Checks; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SlashButton implements Component { + + private String tag; + private String data; + private String label; + private ButtonStyle style; + private String url; + private boolean disabled; + private Emoji emoji; + + /* Constructors */ + + SlashButton(String tag, String data, String label, ButtonStyle style, boolean disabled, Emoji emoji) { + this(tag, data, label, style, null, disabled, emoji); + } + + SlashButton(String tag, String data, String label, ButtonStyle style, String url, boolean disabled, Emoji emoji) { + this.tag = tag; + this.data = data; + this.label = label; + this.style = style; + this.url = url; + this.disabled = disabled; + this.emoji = emoji; + } + + /* Getters & Setters */ + + @NotNull + @Override + public Type getType() { + return Type.BUTTON; + } + + @Nullable + @Override + public String getId() { + return tag == null ? null : ButtonRegistry.getInstance().createButtonId(tag, data); + } + + public String getTag() { + return tag; + } + + public SlashButton withTag(String tag) { + Checks.notEmpty(tag, "Tag"); + this.tag = tag; + return this; + } + + public String getData() { + return data; + } + + public SlashButton withData(String data) { + Checks.notEmpty(data, "Data"); + Checks.notLonger(data, 100 - ButtonRegistry.CODE_LENGTH, "Data"); + this.data = data; + return this; + } + + public String getLabel() { + return label; + } + + public SlashButton withLabel(String label) { + Checks.notEmpty(label, "Label"); + Checks.notLonger(label, 80, "Label"); + this.label = label; + return this; + } + + public ButtonStyle getStyle() { + return style; + } + + public String getUrl() { + return url; + } + + public SlashButton withUrl(String url) { + Checks.notEmpty(url, "Url"); + Checks.notLonger(url, 512, "Url"); + this.url = url; + this.style = ButtonStyle.LINK; + return this; + } + + public Emoji getEmoji() { + return emoji; + } + + public SlashButton withEmoji(Emoji emoji) { + this.emoji = emoji; + return this; + } + + public boolean isDisabled() { + return disabled; + } + + public SlashButton asDisabled() { + this.disabled = true; + return this; + } + + public SlashButton asEnabled() { + this.disabled = false; + return this; + } + + public SlashButton withDisabled(boolean disabled) { + this.disabled = disabled; + return this; + } + + /* Methods */ + + public static SlashButton primary(String tag, String label) { + Checks.notEmpty(tag, "Tag"); + Checks.notEmpty(label, "Label"); + Checks.notLonger(label, 80, "Label"); + return new SlashButton(tag, null, label, ButtonStyle.PRIMARY, false, null); + } + + public static SlashButton primary(String tag, Emoji emoji) { + Checks.notEmpty(tag, "Tag"); + Checks.notNull(emoji, "Emoji"); + return new SlashButton(tag, null, "", ButtonStyle.PRIMARY, false, emoji); + } + + public static SlashButton secondary(String tag, String label) { + Checks.notEmpty(tag, "Tag"); + Checks.notEmpty(label, "Label"); + Checks.notLonger(label, 80, "Label"); + return new SlashButton(tag, null, label, ButtonStyle.SECONDARY, false, null); + } + + public static SlashButton secondary(String tag, Emoji emoji) { + Checks.notEmpty(tag, "Tag"); + Checks.notNull(emoji, "Emoji"); + return new SlashButton(tag, null, "", ButtonStyle.SECONDARY, false, emoji); + } + + public static SlashButton success(String tag, String label) { + Checks.notEmpty(tag, "Tag"); + Checks.notEmpty(label, "Label"); + Checks.notLonger(label, 80, "Label"); + return new SlashButton(tag, null, label, ButtonStyle.SUCCESS, false, null); + } + + public static SlashButton success(String tag, Emoji emoji) { + Checks.notEmpty(tag, "Tag"); + Checks.notNull(emoji, "Emoji"); + return new SlashButton(tag, null, "", ButtonStyle.SUCCESS, false, emoji); + } + + public static SlashButton danger(String tag, String label) { + Checks.notEmpty(tag, "Tag"); + Checks.notEmpty(label, "Label"); + Checks.notLonger(label, 80, "Label"); + return new SlashButton(tag, null, label, ButtonStyle.DANGER, false, null); + } + + public static SlashButton danger(String tag, Emoji emoji) { + Checks.notEmpty(tag, "Tag"); + Checks.notNull(emoji, "Emoji"); + return new SlashButton(tag, null, "", ButtonStyle.DANGER, false, emoji); + } + + public static SlashButton link(String url, String label) { + Checks.notEmpty(url, "Url"); + Checks.notEmpty(label, "Label"); + Checks.notLonger(url, 512, "Url"); + Checks.notLonger(label, 80, "Label"); + return new SlashButton(null, null, label, ButtonStyle.LINK, url, false, null); + } + + public static SlashButton link(String url, Emoji emoji) { + Checks.notEmpty(url, "Url"); + Checks.notLonger(url, 512, "Url"); + Checks.notNull(emoji, "Emoji"); + return new SlashButton(null, null, "", ButtonStyle.LINK, url, false, null); + } + + public static SlashButton of(ButtonStyle style, String tagOrUrl, String label) { + Checks.check(style != ButtonStyle.UNKNOWN, "The button style cannot be UNKNOWN!"); + Checks.notNull(style, "Style"); + if (style == ButtonStyle.LINK) { + return link(tagOrUrl, label); + } + Checks.notNull(label, "Label"); + Checks.notLonger(label, 80, "Label"); + Checks.notEmpty(tagOrUrl, "Tag"); + return new SlashButton(tagOrUrl, null, label, style, false, null); + } + + public static SlashButton of(ButtonStyle style, String tagOrUrl, Emoji emoji) { + Checks.check(style != ButtonStyle.UNKNOWN, "The button style cannot be UNKNOWN!"); + Checks.notNull(style, "Style"); + if (style == ButtonStyle.LINK) { + return link(tagOrUrl, emoji); + } + Checks.notNull(emoji, "Emoji"); + Checks.notEmpty(tagOrUrl, "Tag"); + return new SlashButton(tagOrUrl, null, "", style, false, emoji); + } + + public static SlashButton of(ButtonStyle style, String tagOrUrl, String label, Emoji emoji) { + if (label != null) { + return of(style, tagOrUrl, label).withEmoji(emoji); + } else if (emoji != null) { + return of(style, tagOrUrl, emoji); + } + throw new IllegalArgumentException("Cannot build a button without a label and emoji. At least one has to be provided as non-null."); + } + + @NotNull + @Override + public DataObject toData() { + final DataObject json = DataObject.empty(); + + json.put("type", 2); + json.put("label", label); + json.put("style", style.getKey()); + json.put("disabled", disabled); + if (emoji != null) { + json.put("emoji", emoji); + } + if (url != null) { + json.put("url", url); + } else { + json.put("custom_id", getId()); + } + return json; + } +} From 211bddd9c4ad51cf241eae839c18473bbaabc4ed Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 08:02:08 +0100 Subject: [PATCH 33/42] updated: added a button callback to the PingCommand class example --- .../playground/commands/PingCommand.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java b/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java index 3cbf786..05f0605 100644 --- a/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java +++ b/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java @@ -17,8 +17,10 @@ package com.github.azzerial.slash.playground.commands; import com.github.azzerial.slash.annotations.Slash; +import com.github.azzerial.slash.components.SlashButton; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; @Slash.Tag("ping") @@ -38,6 +40,9 @@ public void ping(SlashCommandEvent event) { event.deferReply(true) .addEmbeds(initialMessage) + .addActionRow( + SlashButton.primary("ping.refresh", "Refresh") + ) .flatMap(v -> { final long latency = System.currentTimeMillis() - time; final String ms = Long.toUnsignedString(latency); @@ -47,6 +52,20 @@ public void ping(SlashCommandEvent event) { .queue(); } + @Slash.Button("ping.refresh") + public void onRefresh(ButtonClickEvent event) { + final long time = System.currentTimeMillis(); + + event.deferEdit() + .flatMap(v -> { + final long latency = System.currentTimeMillis() - time; + final String ms = String.format("%3d", latency); + final MessageEmbed message = buildPingMessage(ms); + return event.getHook().editOriginalEmbeds(message); + }) + .queue(); + } + /* Internal */ private MessageEmbed buildPingMessage(String ms) { From 37757901bbaaa0b65e314e0fbe076c21a6a1c48b Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 08:04:03 +0100 Subject: [PATCH 34/42] fixed: removed unused imports --- .../java/com/github/azzerial/slash/components/SlashButton.java | 1 - .../github/azzerial/slash/playground/commands/PingCommand.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/components/SlashButton.java b/api/src/main/java/com/github/azzerial/slash/components/SlashButton.java index 75031c6..35d5295 100644 --- a/api/src/main/java/com/github/azzerial/slash/components/SlashButton.java +++ b/api/src/main/java/com/github/azzerial/slash/components/SlashButton.java @@ -21,7 +21,6 @@ import net.dv8tion.jda.api.interactions.components.ButtonStyle; import net.dv8tion.jda.api.interactions.components.Component; import net.dv8tion.jda.api.utils.data.DataObject; -import net.dv8tion.jda.internal.interactions.ButtonImpl; import net.dv8tion.jda.internal.utils.Checks; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java b/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java index 05f0605..f8622c2 100644 --- a/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java +++ b/playground/src/main/java/com/github/azzerial/slash/playground/commands/PingCommand.java @@ -59,7 +59,7 @@ public void onRefresh(ButtonClickEvent event) { event.deferEdit() .flatMap(v -> { final long latency = System.currentTimeMillis() - time; - final String ms = String.format("%3d", latency); + final String ms = Long.toUnsignedString(latency); final MessageEmbed message = buildPingMessage(ms); return event.getHook().editOriginalEmbeds(message); }) From 0c2b82e6b6a50276ea8fa37476f0fe6fa838ac27 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 08:47:03 +0100 Subject: [PATCH 35/42] updated: button ids are now trimmed upon creation --- .../java/com/github/azzerial/slash/internal/ButtonRegistry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/github/azzerial/slash/internal/ButtonRegistry.java b/api/src/main/java/com/github/azzerial/slash/internal/ButtonRegistry.java index 39f3a11..ac4dd36 100644 --- a/api/src/main/java/com/github/azzerial/slash/internal/ButtonRegistry.java +++ b/api/src/main/java/com/github/azzerial/slash/internal/ButtonRegistry.java @@ -51,7 +51,7 @@ public String createButtonId(String tag, String data) { "%." + (100 - CODE_LENGTH) + "s", Integer.toUnsignedString(code == -1 ? 0 : code, ID_BASE), data == null ? "" : data - ); + ).trim(); } public ButtonCallback getButtonCallback(String id) { From 409a049a2da2b212b27408206785b848d9bad3a8 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 08:47:40 +0100 Subject: [PATCH 36/42] added: Buffer class --- .../github/azzerial/slash/util/Buffer.java | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/util/Buffer.java diff --git a/api/src/main/java/com/github/azzerial/slash/util/Buffer.java b/api/src/main/java/com/github/azzerial/slash/util/Buffer.java new file mode 100644 index 0000000..297c405 --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/util/Buffer.java @@ -0,0 +1,157 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.util; + +import com.github.azzerial.slash.internal.ButtonRegistry; + +public final class Buffer { + + /* Nested Classes */ + + public static final class Reader { + + private final String buffer; + + private int i = ButtonRegistry.CODE_LENGTH; + + /* Constructors */ + + public Reader(String buffer) { + this.buffer = buffer; + } + + /* Methods */ + + public static Reader of(String buffer) { + return new Reader(buffer); + } + + public Data read(int size) { + final String s = buffer.substring(i, Math.min(buffer.length(), i + size)); + + i += s.length(); + return new Data(s); + } + + /* Nested Classes */ + + public static final class Data { + + private final String s; + + /* Constructors */ + + private Data(String s) { + this.s = s; + } + + /* Methods */ + + public String asString() { + return s.trim(); + } + + public boolean asBoolean() { + return asInt(2) == 1; + } + + public int asInt() { + return Integer.parseInt(s.trim()); + } + + public int asInt(int base) { + return Integer.parseInt(s.trim(), base); + } + + public int asUnsignedInt() { + return Integer.parseUnsignedInt(s.trim()); + } + + public int asUnsignedInt(int base) { + return Integer.parseUnsignedInt(s.trim(), base); + } + + public long asLong() { + return Long.parseLong(s.trim()); + } + + public long asLong(int base) { + return Long.parseLong(s.trim(), base); + } + + public long asUnsignedLong() { + return Long.parseUnsignedLong(s.trim()); + } + + public long asUnsignedLong(int base) { + return Long.parseUnsignedLong(s.trim(), base); + } + + public boolean isEmpty() { + return s.isEmpty(); + } + } + } + + public static final class Writer { + + private final StringBuilder sb = new StringBuilder(); + + /* Constructors */ + + public Writer() {} + + /* Methods */ + + public static Writer create() { + return new Writer(); + } + + public Writer write(int size, String s) { + if (sb.length() + size > 100 - ButtonRegistry.CODE_LENGTH) { + throw new OutOfMemoryError("Required allocation size is greater than the available one!"); + } + sb.append(String.format("%-" + size + "." + size + "s", s)); + return this; + } + + public Writer write(boolean b) { + return write(1, b ? "1" : "0"); + } + + public Writer write(int size, int i) { + return write(size, i, 10); + } + + public Writer write(int size, int i, int base) { + return write(size, Integer.toString(i, base)); + } + + public Writer write(int size, long l) { + return write(size, l, 10); + } + + public Writer write(int size, long l, int base) { + return write(size, Long.toString(l, base)); + } + + @Override + public String toString() { + return sb.toString(); + } + } +} From a6e180c6b4ee91ce979f75e5a56934f03a3cc6ae Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 10:18:54 +0100 Subject: [PATCH 37/42] fixed: added exception on missing code in Buffer.Reader class --- api/src/main/java/com/github/azzerial/slash/util/Buffer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/src/main/java/com/github/azzerial/slash/util/Buffer.java b/api/src/main/java/com/github/azzerial/slash/util/Buffer.java index 297c405..17ed6a6 100644 --- a/api/src/main/java/com/github/azzerial/slash/util/Buffer.java +++ b/api/src/main/java/com/github/azzerial/slash/util/Buffer.java @@ -17,6 +17,7 @@ package com.github.azzerial.slash.util; import com.github.azzerial.slash.internal.ButtonRegistry; +import net.dv8tion.jda.internal.utils.Checks; public final class Buffer { @@ -31,6 +32,10 @@ public static final class Reader { /* Constructors */ public Reader(String buffer) { + Checks.notNull(buffer, "Buffer"); + if (buffer.length() <= i) { + throw new IllegalArgumentException("The buffer is invalid!"); + } this.buffer = buffer; } From d0d25ad1db5f37c281b2c9ad31e28cc42a427495 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Fri, 11 Jun 2021 10:22:04 +0100 Subject: [PATCH 38/42] added: Session class --- .../github/azzerial/slash/util/Session.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 api/src/main/java/com/github/azzerial/slash/util/Session.java diff --git a/api/src/main/java/com/github/azzerial/slash/util/Session.java b/api/src/main/java/com/github/azzerial/slash/util/Session.java new file mode 100644 index 0000000..66d49fd --- /dev/null +++ b/api/src/main/java/com/github/azzerial/slash/util/Session.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021 Robin Mercier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.azzerial.slash.util; + +import com.github.azzerial.slash.internal.ButtonRegistry; +import net.dv8tion.jda.api.interactions.InteractionHook; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.internal.utils.Checks; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public final class Session extends DataObject { + + private static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"; + + private static final ScheduledExecutorService threadpool = Executors.newSingleThreadScheduledExecutor(); + private static final Map sessions = new HashMap<>(); + + private final UUID uuid; + + private boolean isTimeoutSet; + + /* Constructors */ + + private Session(UUID uuid) { + super(new HashMap<>()); + this.uuid = uuid; + } + + /* Getters & Setters */ + + public String getUuid() { + return uuid.toString(); + } + + /* Methods */ + + public static Session create() { + final UUID uuid = UUID.randomUUID(); + final Session session = new Session(uuid); + + sessions.put(uuid, session); + return session; + } + + public static Session load(String id) { + Checks.notNull(id, "Id"); + if (id.length() != ButtonRegistry.CODE_LENGTH + 36 && !id.matches(UUID_REGEX)) { + throw new IllegalArgumentException("The id is invalid!"); + } + final UUID uuid = UUID.fromString(id.substring(ButtonRegistry.CODE_LENGTH, ButtonRegistry.CODE_LENGTH + 36)); + return sessions.remove(uuid); + } + + public Session setTimeout(long timeout, TimeUnit unit) { + return setTimeout(timeout, unit, null, null); + } + + public Session setTimeout(long timeout, TimeUnit unit, InteractionHook hook, Consumer action) { + if (isTimeoutSet) { + throw new IllegalStateException("The timeout has already been set!"); + } + Checks.positive(timeout, "Timeout"); + Checks.notNull(unit, "Unit"); + this.isTimeoutSet = true; + threadpool.schedule(() -> { + if (sessions.remove(uuid) != null && hook != null && action != null) { + action.accept(hook); + } + }, timeout, unit); + return this; + } + + @NotNull + @Override + public Session remove(@NotNull String key) { + super.remove(key); + return this; + } + + @NotNull + @Override + public Session putNull(@NotNull String key) { + super.putNull(key); + return this; + } + + @NotNull + @Override + public Session put(@NotNull String key, @Nullable Object value) { + super.put(key, value); + return this; + } + + @NotNull + @Override + public Session toData() { + super.toData(); + return this; + } +} From 6a2550dab72e82e84c6ecb1b8117e9c414d71ba3 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Sat, 12 Jun 2021 04:32:09 +0100 Subject: [PATCH 39/42] feature: added unique session stores and session timeouts --- .../github/azzerial/slash/util/Session.java | 139 +++++++++++++----- 1 file changed, 106 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/util/Session.java b/api/src/main/java/com/github/azzerial/slash/util/Session.java index 66d49fd..44c4a18 100644 --- a/api/src/main/java/com/github/azzerial/slash/util/Session.java +++ b/api/src/main/java/com/github/azzerial/slash/util/Session.java @@ -16,7 +16,6 @@ package com.github.azzerial.slash.util; -import com.github.azzerial.slash.internal.ButtonRegistry; import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.utils.data.DataObject; import net.dv8tion.jda.internal.utils.Checks; @@ -28,25 +27,48 @@ import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import static com.github.azzerial.slash.internal.ButtonRegistry.CODE_LENGTH; + public final class Session extends DataObject { + public static final long DEFAULT_TIMEOUT = 60_000L; + public static final TimeUnit DEFAULT_TIMEOUT_UNIT = TimeUnit.MILLISECONDS; + private static final String UUID_REGEX = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"; private static final ScheduledExecutorService threadpool = Executors.newSingleThreadScheduledExecutor(); private static final Map sessions = new HashMap<>(); private final UUID uuid; + private final Map storage = new HashMap<>(); - private boolean isTimeoutSet; + private final long timeout; + private final TimeUnit unit; + private final InteractionHook hook; + private final Consumer action; + private ScheduledFuture thread; /* Constructors */ - private Session(UUID uuid) { - super(new HashMap<>()); + private Session(Session session, Map data) { + this(session.uuid, data, session.timeout, session.unit, session.hook, session.action); + } + + private Session(UUID uuid, Map data, long timeout, TimeUnit unit, InteractionHook hook, Consumer action) { + super(data); + Checks.notNull(uuid, "UUID"); + Checks.notNegative(timeout, "Timeout"); this.uuid = uuid; + this.timeout = timeout; + this.unit = unit; + this.hook = hook; + this.action = action; + + startTimeoutThread(); } /* Getters & Setters */ @@ -58,39 +80,30 @@ public String getUuid() { /* Methods */ public static Session create() { + return create(DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_UNIT, null, null); + } + + public static Session create(long timeout, TimeUnit unit) { + return create(timeout, unit, null, null); + } + + public static Session create(long timeout, TimeUnit unit, InteractionHook hook, Consumer action) { + Checks.positive(timeout, "Timeout"); + Checks.notNull(unit, "Unit"); final UUID uuid = UUID.randomUUID(); - final Session session = new Session(uuid); + final Session session = new Session(uuid, new HashMap<>(), timeout, unit, hook, action); + System.out.println("created: " + uuid); sessions.put(uuid, session); return session; } public static Session load(String id) { - Checks.notNull(id, "Id"); - if (id.length() != ButtonRegistry.CODE_LENGTH + 36 && !id.matches(UUID_REGEX)) { - throw new IllegalArgumentException("The id is invalid!"); - } - final UUID uuid = UUID.fromString(id.substring(ButtonRegistry.CODE_LENGTH, ButtonRegistry.CODE_LENGTH + 36)); - return sessions.remove(uuid); - } - - public Session setTimeout(long timeout, TimeUnit unit) { - return setTimeout(timeout, unit, null, null); + return get(id, false); } - public Session setTimeout(long timeout, TimeUnit unit, InteractionHook hook, Consumer action) { - if (isTimeoutSet) { - throw new IllegalStateException("The timeout has already been set!"); - } - Checks.positive(timeout, "Timeout"); - Checks.notNull(unit, "Unit"); - this.isTimeoutSet = true; - threadpool.schedule(() -> { - if (sessions.remove(uuid) != null && hook != null && action != null) { - action.accept(hook); - } - }, timeout, unit); - return this; + public static Session renew(String id) { + return get(id, true); } @NotNull @@ -114,10 +127,70 @@ public Session put(@NotNull String key, @Nullable Object value) { return this; } - @NotNull - @Override - public Session toData() { - super.toData(); - return this; + public String store(Consumer consumer) { + final UUID uuid = UUID.randomUUID(); + final DataObject data = DataObject.fromJson(toJson()); + + consumer.accept(data); + storage.put(uuid, data); + return getUuid() + uuid; + } + + /* Internal */ + + private static Session get(String id, boolean renew) { + Checks.notNull(id, "Id"); + if (id.length() != CODE_LENGTH + 36 && id.length() != CODE_LENGTH + 72) { + throw new IllegalArgumentException("The id is invalid!"); + } + + final String sessionStr = id.substring(CODE_LENGTH, CODE_LENGTH + 36); + final String storageStr = id.length() == CODE_LENGTH + 72 ? + id.substring(CODE_LENGTH +36, CODE_LENGTH + 72) : + null; + + if (!sessionStr.matches(UUID_REGEX) || (storageStr != null && !storageStr.matches(UUID_REGEX))) { + throw new IllegalArgumentException("The id is invalid!"); + } + + final UUID sessionUuid = UUID.fromString(sessionStr); + Session session = sessions.remove(sessionUuid); + + if (session == null) { + return null; + } else if (session.thread != null && !session.thread.isDone()) { + session.thread.cancel(true); + } + + final UUID storageUuid = storageStr != null ? UUID.fromString(storageStr) : null; + final DataObject data = session.storage.get(storageUuid); + + if (data != null && !data.keys().isEmpty()) { + session = new Session(session, data.toMap()); + } + if (renew) { + session.startTimeoutThread(); + System.out.println("renewed: " + session.uuid); + } else { + System.out.println("loaded: " + session.uuid); + } + return session; + } + + private void startTimeoutThread() { + if (timeout > 0 && unit != null) { + if (thread != null && !thread.isDone()) { + return; + } + + this.thread = threadpool.schedule(() -> { + System.out.println("destroyed: " + uuid); + if (sessions.remove(uuid) != null && hook != null && action != null) { + action.accept(hook); + } + }, timeout, unit); + } else { + this.thread = null; + } } } From 5b35297156b9d7e86ee4c22dbcada542033d34e7 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Sun, 13 Jun 2021 01:06:00 +0100 Subject: [PATCH 40/42] updated: added session object to session timeout action and fixed session renew --- .../com/github/azzerial/slash/util/Session.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/com/github/azzerial/slash/util/Session.java b/api/src/main/java/com/github/azzerial/slash/util/Session.java index 44c4a18..d2926f6 100644 --- a/api/src/main/java/com/github/azzerial/slash/util/Session.java +++ b/api/src/main/java/com/github/azzerial/slash/util/Session.java @@ -29,6 +29,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.function.Consumer; import static com.github.azzerial.slash.internal.ButtonRegistry.CODE_LENGTH; @@ -49,7 +50,7 @@ public final class Session extends DataObject { private final long timeout; private final TimeUnit unit; private final InteractionHook hook; - private final Consumer action; + private final BiConsumer action; private ScheduledFuture thread; /* Constructors */ @@ -58,7 +59,7 @@ private Session(Session session, Map data) { this(session.uuid, data, session.timeout, session.unit, session.hook, session.action); } - private Session(UUID uuid, Map data, long timeout, TimeUnit unit, InteractionHook hook, Consumer action) { + private Session(UUID uuid, Map data, long timeout, TimeUnit unit, InteractionHook hook, BiConsumer action) { super(data); Checks.notNull(uuid, "UUID"); Checks.notNegative(timeout, "Timeout"); @@ -87,13 +88,12 @@ public static Session create(long timeout, TimeUnit unit) { return create(timeout, unit, null, null); } - public static Session create(long timeout, TimeUnit unit, InteractionHook hook, Consumer action) { + public static Session create(long timeout, TimeUnit unit, InteractionHook hook, BiConsumer action) { Checks.positive(timeout, "Timeout"); Checks.notNull(unit, "Unit"); final UUID uuid = UUID.randomUUID(); final Session session = new Session(uuid, new HashMap<>(), timeout, unit, hook, action); - System.out.println("created: " + uuid); sessions.put(uuid, session); return session; } @@ -170,9 +170,7 @@ private static Session get(String id, boolean renew) { } if (renew) { session.startTimeoutThread(); - System.out.println("renewed: " + session.uuid); - } else { - System.out.println("loaded: " + session.uuid); + sessions.put(session.uuid, session); } return session; } @@ -184,9 +182,8 @@ private void startTimeoutThread() { } this.thread = threadpool.schedule(() -> { - System.out.println("destroyed: " + uuid); if (sessions.remove(uuid) != null && hook != null && action != null) { - action.accept(hook); + action.accept(hook, this); } }, timeout, unit); } else { From 269d2c27a286867376db4de67b86a0bfee085d3c Mon Sep 17 00:00:00 2001 From: Azzerial Date: Sun, 13 Jun 2021 04:29:08 +0100 Subject: [PATCH 41/42] updated: changed jitpack development branch to JDA 4.3.0 official release --- api/build.gradle | 2 +- build.gradle | 1 - playground/build.gradle | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 79c0b3f..04a250e 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -23,5 +23,5 @@ ext.moduleName = 'api' archivesBaseName = moduleName dependencies { - implementation ('com.github.DV8FromTheWorld:JDA:development-SNAPSHOT') + implementation ('net.dv8tion:JDA:4.3.0_277') } diff --git a/build.gradle b/build.gradle index 980ed49..115817d 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,6 @@ subprojects { repositories { mavenCentral() - maven { url 'https://jitpack.io' } maven { url 'https://m2.dv8tion.net/releases' } } diff --git a/playground/build.gradle b/playground/build.gradle index 3bcc915..a51ac39 100644 --- a/playground/build.gradle +++ b/playground/build.gradle @@ -21,7 +21,7 @@ plugins { dependencies { implementation project(':api') implementation ('ch.qos.logback:logback-classic:1.2.3') - implementation ('com.github.DV8FromTheWorld:JDA:development-SNAPSHOT') + implementation ('net.dv8tion:JDA:4.3.0_277') } java { From a83a43d5debc33d49a602e07c320696c7c3a1492 Mon Sep 17 00:00:00 2001 From: Azzerial Date: Sun, 13 Jun 2021 06:47:04 +0100 Subject: [PATCH 42/42] updated: README.md --- README.md | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c77a8ee..61929b4 100644 --- a/README.md +++ b/README.md @@ -9,40 +9,53 @@

FeaturesHow To Use • - Related • - ContributingLicense

+ +
## Features A few of the things you can do with Slash Commands: -* some feature -* another feature +* Create and manage Slash Commands +* Assign callbacks to Slash Commands events (supports per [command path](https://ci.dv8tion.net/job/JDA/javadoc/net/dv8tion/jda/api/interactions/commands/CommandInteraction.html#getCommandPath()) callbacks) +* Assign callbacks to buttons +* Session system for interactions (with session store) ## How to Use ```java -public class Example { +@Slash.Tag("slash_cmd") +@Slash.Command( + name = "slash-command", + description = "A proof of concept Slash Command" +) +public final class SlashCommand { + + public static void main(String[] args) throws LoginException, InterruptedException { + final JDA jda = JDABuilder.createDefault(...) + .build() + .awaitReady(); + final SlashClient slash = SlashClientBuilder.create(jda) + .addCommand(new SlashCommand()) // register your commands + .build(); + + slash.getCommand("slash_cmd") // get a SlashCommand by it's @Slash.Tag + .upsertGuild(...); // upsert as a guild Slash Command + } - public static void main(String[] args) { - System.out.println("Hello World"); + @Slash.Handler + public void callback(SlashCommandEvent event) { + event.deferReply() + .setContent("Hello World!") + .queue(); } } ``` -*For more examples and usage, please refer to the [wiki](wiki).* - -## Related - -* some project -* another project - -## Contributing - -Your contributions are always welcome! Please take a moment to read the [contribution guidelines](CONTRIBUTING.md) first. +*For more examples and usage, please refer to the [playground module](playground/).* ## License