diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/component/Interaction.java b/api/src/main/java/com/discordsrv/api/discord/entity/Interaction.java similarity index 92% rename from api/src/main/java/com/discordsrv/api/discord/entity/component/Interaction.java rename to api/src/main/java/com/discordsrv/api/discord/entity/Interaction.java index 54a56d5e..69c03a87 100644 --- a/api/src/main/java/com/discordsrv/api/discord/entity/component/Interaction.java +++ b/api/src/main/java/com/discordsrv/api/discord/entity/Interaction.java @@ -21,10 +21,9 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.entity.component; +package com.discordsrv.api.discord.entity; -import com.discordsrv.api.discord.entity.DiscordUser; -import com.discordsrv.api.discord.entity.JDAEntity; +import com.discordsrv.api.discord.entity.component.impl.Modal; import com.discordsrv.api.discord.entity.message.ReceivedDiscordMessage; import com.discordsrv.api.discord.entity.message.SendableDiscordMessage; import net.dv8tion.jda.api.interactions.InteractionHook; diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordChannelType.java b/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordChannelType.java new file mode 100644 index 00000000..cf5068ff --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordChannelType.java @@ -0,0 +1,56 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.entity.channel; + +import com.discordsrv.api.discord.entity.JDAEntity; +import net.dv8tion.jda.api.entities.ChannelType; + +/** + * Represents a Discord channel type. + */ +public enum DiscordChannelType implements JDAEntity { + + TEXT(ChannelType.TEXT), + PRIVATE(ChannelType.PRIVATE), + VOICE(ChannelType.VOICE), + GROUP(ChannelType.GROUP), + CATEGORY(ChannelType.CATEGORY), + NEWS(ChannelType.NEWS), + STAGE(ChannelType.STAGE), + GUILD_NEWS_THREAD(ChannelType.GUILD_NEWS_THREAD), + GUILD_PUBLIC_THREAD(ChannelType.GUILD_PUBLIC_THREAD), + GUILD_PRIVATE_THREAD(ChannelType.GUILD_PRIVATE_THREAD), + ; + + private final ChannelType jda; + + DiscordChannelType(ChannelType jda) { + this.jda = jda; + } + + @Override + public ChannelType asJDA() { + return jda; + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordDMChannel.java b/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordDMChannel.java index 6a845dae..9b0337b5 100644 --- a/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordDMChannel.java +++ b/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordDMChannel.java @@ -40,4 +40,8 @@ public interface DiscordDMChannel extends DiscordMessageChannel, JDAEntity {} +public interface DiscordNewsChannel extends DiscordGuildMessageChannel, DiscordThreadContainer, JDAEntity { + + @Override + default DiscordChannelType getType() { + return DiscordChannelType.NEWS; + } + +} diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordTextChannel.java b/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordTextChannel.java index 4bb07b8b..92b828ec 100644 --- a/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordTextChannel.java +++ b/api/src/main/java/com/discordsrv/api/discord/entity/channel/DiscordTextChannel.java @@ -39,4 +39,8 @@ public interface DiscordTextChannel extends DiscordGuildMessageChannel, DiscordT @Nullable String getTopic(); + @Override + default DiscordChannelType getType() { + return DiscordChannelType.TEXT; + } } diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/command/Command.java b/api/src/main/java/com/discordsrv/api/discord/entity/command/Command.java new file mode 100644 index 00000000..8d9f1143 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/entity/command/Command.java @@ -0,0 +1,377 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.entity.command; + +import com.discordsrv.api.discord.entity.JDAEntity; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions; +import net.dv8tion.jda.api.interactions.commands.build.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.*; + +public class Command implements JDAEntity { + + /** + * Creates a chat input or slash command builder. + * + * @param name the name of the command + * @param description the description of the command + * @return a new chat input command builder + */ + public static ChatInputBuilder chatInput(String name, String description) { + return new ChatInputBuilder(name, description); + } + + /** + * Creates a new user context menu command. + * + * @param name the name of the command + * @return a new command builder + */ + public static Builder user(String name) { + return new Builder(Type.USER, name); + } + + /** + * Creates a new message context menu command. + * + * @param name the name of the command + * @return a new command builder + */ + public static Builder message(String name) { + return new Builder(Type.MESSAGE, name); + } + + private final Type type; + private final Map nameTranslations; + private final Map descriptionTranslations; + private final List subCommandGroups; + private final List subCommands; + private final List options; + private final boolean guildOnly; + private final DefaultPermission defaultPermission; + + private Command( + Type type, + Map nameTranslations, + Map descriptionTranslations, + List subCommandGroups, + List subCommands, + List options, + boolean guildOnly, + DefaultPermission defaultPermission + ) { + this.type = type; + this.nameTranslations = nameTranslations; + this.descriptionTranslations = descriptionTranslations; + this.subCommandGroups = subCommandGroups; + this.subCommands = subCommands; + this.options = options; + this.guildOnly = guildOnly; + this.defaultPermission = defaultPermission; + } + + @NotNull + public Type getType() { + return type; + } + + @NotNull + public String getName() { + return nameTranslations.get(Locale.ROOT); + } + + @NotNull + @Unmodifiable + public Map getNameTranslations() { + return Collections.unmodifiableMap(nameTranslations); + } + + @Nullable + public String getDescription() { + return descriptionTranslations.get(Locale.ROOT); + } + + @NotNull + @Unmodifiable + public Map getDescriptionTranslations() { + return Collections.unmodifiableMap(descriptionTranslations); + } + + @NotNull + @Unmodifiable + public List getSubCommandGroups() { + return Collections.unmodifiableList(subCommandGroups); + } + + @NotNull + @Unmodifiable + public List getOptions() { + return Collections.unmodifiableList(options); + } + + public boolean isGuildOnly() { + return guildOnly; + } + + @NotNull + public DefaultPermission getDefaultPermission() { + return defaultPermission; + } + + @Override + public CommandData asJDA() { + CommandData commandData; + switch (type) { + case USER: + commandData = Commands.user(getName()); + break; + case MESSAGE: + commandData = Commands.message(getName()); + break; + case CHAT_INPUT: + SlashCommandData slashCommandData = Commands.slash(getName(), Objects.requireNonNull(getDescription())); + slashCommandData.addSubcommandGroups(subCommandGroups.stream().map(JDAEntity::asJDA).toArray(SubcommandGroupData[]::new)); + slashCommandData.addSubcommands(subCommands.stream().map(Command::asJDASubcommand).toArray(SubcommandData[]::new)); + slashCommandData.addOptions(options.stream().map(JDAEntity::asJDA).toArray(OptionData[]::new)); + commandData = slashCommandData; + break; + default: + throw new IllegalStateException("Missing switch case"); + } + + commandData.setGuildOnly(guildOnly); + commandData.setDefaultPermissions(defaultPermission.asJDA()); + + return commandData; + } + + public SubcommandData asJDASubcommand() { + SubcommandData data = new SubcommandData(nameTranslations.get(Locale.ROOT), descriptionTranslations.get(Locale.ROOT)); + data.addOptions(options.stream().map(JDAEntity::asJDA).toArray(OptionData[]::new)); + return data; + } + + public static class ChatInputBuilder extends Builder { + + private final Map descriptionTranslations = new LinkedHashMap<>(); + private final List subCommandGroups = new ArrayList<>(); + private final List subCommands = new ArrayList<>(); + private final List options = new ArrayList<>(); + + private ChatInputBuilder(String name, String description) { + super(Type.CHAT_INPUT, name); + this.descriptionTranslations.put(Locale.ROOT, description); + } + + /** + * Adds a description translation for this command. + * @param locale the language + * @param translation the translation + * @return this builder, useful for chaining + * @throws IllegalStateException if this isn't a {@link Type#CHAT_INPUT} command + */ + @NotNull + public ChatInputBuilder addDescriptionTranslation(@NotNull Locale locale, @NotNull String translation) { + if (type != Type.CHAT_INPUT) { + throw new IllegalStateException("Descriptions are only available for CHAT_INPUT commands"); + } + this.descriptionTranslations.put(locale, translation); + return this; + } + + /** + * Adds a sub command group to this command. + * + * @param subCommandGroup the sub command group + * @return this builder, useful for chaining + */ + @NotNull + public ChatInputBuilder addSubCommandGroup(@NotNull SubCommandGroup subCommandGroup) { + this.subCommandGroups.add(subCommandGroup); + return this; + } + + /** + * Adds a sub command to this command. + * + * @param command the sub command + * @return this builder, useful for chaining + */ + @NotNull + public ChatInputBuilder addSubCommand(@NotNull Command command) { + this.subCommands.add(command); + return this; + } + + /** + * Adds an option to this command. + * + * @param option the option + * @return this builder, useful for chaining + */ + @NotNull + public ChatInputBuilder addOption(@NotNull CommandOption option) { + this.options.add(option); + return this; + } + + @Override + public Command build() { + return new Command( + type, + nameTranslations, + descriptionTranslations, + subCommandGroups, + subCommands, + options, + guildOnly, + defaultPermission + ); + } + } + + public static class Builder { + + protected final Type type; + protected final Map nameTranslations = new LinkedHashMap<>(); + protected boolean guildOnly = true; + protected DefaultPermission defaultPermission = DefaultPermission.EVERYONE; + + private Builder(Type type, String name) { + this.type = type; + this.nameTranslations.put(Locale.ROOT, name); + } + + /** + * Adds a name translation for this command. + * @param locale the language + * @param translation the translation + * @return this builder, useful for chaining + */ + @NotNull + public Builder addNameTranslation(@NotNull Locale locale, @NotNull String translation) { + this.nameTranslations.put(locale, translation); + return this; + } + + /** + * Sets if this command is limited to Discord servers. + * @param guildOnly if this command is limited to Discord servers + * @return this builder, useful for chaining + */ + @NotNull + public Builder setGuildOnly(boolean guildOnly) { + this.guildOnly = guildOnly; + return this; + } + + /** + * Sets the permission level required to use the command by default. + * @param defaultPermission the permission level + */ + @NotNull + public Builder setDefaultPermission(@NotNull DefaultPermission defaultPermission) { + this.defaultPermission = defaultPermission; + return this; + } + + public Command build() { + return new Command( + type, + nameTranslations, + Collections.emptyMap(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + guildOnly, + defaultPermission + ); + } + } + + public interface DefaultPermission extends JDAEntity { + + DefaultPermission EVERYONE = new Simple(true); + DefaultPermission ADMINISTRATOR = new Simple(false); + + DefaultPermission BAN_MEMBERS = Permissions.fromJDA(Permission.BAN_MEMBERS); + DefaultPermission MODERATE_MEMBERS = Permissions.fromJDA(Permission.MODERATE_MEMBERS); + DefaultPermission MANAGE_PERMISSIONS = Permissions.fromJDA(Permission.MANAGE_PERMISSIONS); + DefaultPermission MESSAGE_MANAGE = Permissions.fromJDA(Permission.MESSAGE_MANAGE); + + class Simple implements DefaultPermission { + + private final boolean value; + + private Simple(boolean value) { + this.value = value; + } + + @Override + public DefaultMemberPermissions asJDA() { + return value ? DefaultMemberPermissions.ENABLED : DefaultMemberPermissions.DISABLED; + } + } + + class Permissions implements DefaultPermission { + + public static Permissions fromJDA(Permission... permissions) { + return new Permissions(Permission.getRaw(permissions)); + } + + private final long permissions; + + public Permissions(long permissions) { + this.permissions = permissions; + } + + @Override + public DefaultMemberPermissions asJDA() { + return DefaultMemberPermissions.enabledFor(permissions); + } + } + } + + public enum Type implements JDAEntity { + + CHAT_INPUT(net.dv8tion.jda.api.interactions.commands.Command.Type.SLASH), + USER(net.dv8tion.jda.api.interactions.commands.Command.Type.USER), + MESSAGE(net.dv8tion.jda.api.interactions.commands.Command.Type.MESSAGE); + + private final net.dv8tion.jda.api.interactions.commands.Command.Type jda; + + Type(net.dv8tion.jda.api.interactions.commands.Command.Type jda) { + this.jda = jda; + } + + @Override + public net.dv8tion.jda.api.interactions.commands.Command.Type asJDA() { + return jda; + } + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/command/CommandOption.java b/api/src/main/java/com/discordsrv/api/discord/entity/command/CommandOption.java new file mode 100644 index 00000000..59969207 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/entity/command/CommandOption.java @@ -0,0 +1,198 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.entity.command; + +import com.discordsrv.api.discord.entity.JDAEntity; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public class CommandOption implements JDAEntity { + + /** + * Creates a new command option builder. + * + * @param type the type of the command option + * @param name the name of the command option + * @param description the description of the command option + * @return the new command option builder + */ + @NotNull + public static Builder builder(@NotNull Type type, @NotNull String name, @NotNull String description) { + return new Builder(type, name, description); + } + + private final Type type; + private final String name; + private final String description; + private final Map choices; + + public CommandOption(Type type, String name, String description, Map choices) { + this.type = type; + this.name = name; + this.description = description; + this.choices = choices; + } + + @NotNull + public Type getType() { + return type; + } + + @NotNull + public String getName() { + return name; + } + + @NotNull + public String getDescription() { + return description; + } + + @NotNull + @Unmodifiable + public Map getChoices() { + return Collections.unmodifiableMap(choices); + } + + @Override + public OptionData asJDA() { + OptionData data = new OptionData(type.asJDA(), name, description); + for (Map.Entry entry : choices.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (value instanceof String) { + data.addChoice(key, (String) value); + } else if (value instanceof Integer) { + data.addChoice(key, (Integer) value); + } else if (value instanceof Double) { + data.addChoice(key, (Double) value); + } else { + throw new IllegalStateException("Not a String, Integer or Double choice value"); + } + } + return data; + } + + public static class Builder { + + private final Type type; + private final String name; + private final String description; + private final Map choices = new LinkedHashMap<>(); + + private Builder(Type type, String name, String description) { + this.type = type; + this.name = name; + this.description = description; + } + + /** + * Adds a String choice, type must be {@link Type#STRING}. + * + * @param name the name of the choice, this will be returned via the event + * @param stringValue the choice + * @return this builder, useful for chaining + */ + @NotNull + public Builder addChoice(String name, String stringValue) { + if (type != Type.DOUBLE) { + throw new IllegalStateException("Must be of type STRING"); + } + this.choices.put(name, stringValue); + return this; + } + + /** + * Adds a String choice, type must be {@link Type#INTEGER}. + * + * @param name the name of the choice, this will be returned via the event + * @param integerValue the choice + * @return this builder, useful for chaining + */ + @NotNull + public Builder addChoice(String name, int integerValue) { + if (type != Type.INTEGER) { + throw new IllegalStateException("Must be of type INTEGER"); + } + this.choices.put(name, integerValue); + return this; + } + + /** + * Adds a String choice, type must be {@link Type#DOUBLE}. + * + * @param name the name of the choice, this will be returned via the event + * @param doubleValue the choice + * @return this builder, useful for chaining + */ + @NotNull + public Builder addChoice(String name, double doubleValue) { + if (type != Type.DOUBLE) { + throw new IllegalStateException("Must be of type DOUBLE"); + } + this.choices.put(name, doubleValue); + return this; + } + + @NotNull + public CommandOption build() { + return new CommandOption(type, name, description, choices); + } + } + + public enum Type implements JDAEntity { + STRING(OptionType.STRING), + INTEGER(OptionType.INTEGER), + DOUBLE(OptionType.NUMBER), + BOOLEAN(OptionType.BOOLEAN), + + USER(OptionType.USER), + CHANNEL(OptionType.CHANNEL), + ROLE(OptionType.ROLE), + MENTIONABLE(OptionType.MENTIONABLE), + + ATTACHMENT(OptionType.ATTACHMENT); + + private final OptionType optionType; + + Type(OptionType optionType) { + this.optionType = optionType; + } + + public boolean isSupportsChoices() { + return optionType.canSupportChoices(); + } + + @Override + public OptionType asJDA() { + return optionType; + } + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/command/SubCommandGroup.java b/api/src/main/java/com/discordsrv/api/discord/entity/command/SubCommandGroup.java new file mode 100644 index 00000000..e93308f6 --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/entity/command/SubCommandGroup.java @@ -0,0 +1,81 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.entity.command; + +import com.discordsrv.api.discord.entity.JDAEntity; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Arrays; +import java.util.List; + +public class SubCommandGroup implements JDAEntity { + + /** + * Creates a sub command group. + * + * @param name the sub command group name + * @param description the sub command group description + * @param commands the commands within the sub command group + * @return a new sub command group + */ + @NotNull + public static SubCommandGroup of(@NotNull String name, @NotNull String description, @NotNull Command... commands) { + return new SubCommandGroup(name, description, Arrays.asList(commands)); + } + + private final String name; + private final String description; + private final List commands; + + private SubCommandGroup(String name, String description, List commands) { + this.name = name; + this.description = description; + this.commands = commands; + } + + @NotNull + public String getName() { + return name; + } + + @NotNull + public String getDescription() { + return description; + } + + @NotNull + @Unmodifiable + public List getCommands() { + return commands; + } + + @Override + public SubcommandGroupData asJDA() { + return new SubcommandGroupData(name, description) + .addSubcommands(commands.stream().map(Command::asJDASubcommand).toArray(SubcommandData[]::new)); + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/component/ComponentIdentifier.java b/api/src/main/java/com/discordsrv/api/discord/entity/component/ComponentIdentifier.java new file mode 100644 index 00000000..b995ad0d --- /dev/null +++ b/api/src/main/java/com/discordsrv/api/discord/entity/component/ComponentIdentifier.java @@ -0,0 +1,102 @@ +/* + * This file is part of the DiscordSRV API, licensed under the MIT License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.discordsrv.api.discord.entity.component; + +import org.intellij.lang.annotations.Pattern; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/** + * An identifier for components to match up with interaction events. + */ +public class ComponentIdentifier { + + public static final String ID_PREFIX = "DiscordSRV/"; + + private static final String REGEX = "[\\w\\d-_]{1,40}"; + private static final java.util.regex.Pattern PATTERN = java.util.regex.Pattern.compile(REGEX); + + /** + * Creates a new {@link ComponentIdentifier}. + * + * @param extensionName the name of the plugin or mod that owns this identifier (1-40 characters, a-z, A-Z, 0-9, -, _) + * @param identifier the identifier of this component (1-40 characters, a-z, A-Z, 0-9, -, _) + * @return a new {@link ComponentIdentifier} + * @throws IllegalArgumentException if the extension name or identifier does not match the required constraints + */ + @NotNull + public static ComponentIdentifier of( + @NotNull @Pattern(REGEX) String extensionName, + @NotNull @Pattern(REGEX) String identifier + ) { + if (!PATTERN.matcher(extensionName).matches()) { + throw new IllegalArgumentException("Extension name does not match the required pattern"); + } else if (!PATTERN.matcher(identifier).matches()) { + throw new IllegalArgumentException("Identifier does not match the required pattern"); + } + return new ComponentIdentifier(extensionName, identifier); + } + + private final String extensionName; + private final String identifier; + + private ComponentIdentifier(String extensionName, String identifier) { + this.extensionName = extensionName; + this.identifier = identifier; + } + + public String getExtensionName() { + return extensionName; + } + + public String getIdentifier() { + return identifier; + } + + public String getDiscordIdentifier() { + return ID_PREFIX + getExtensionName() + ":" + getIdentifier(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ComponentIdentifier that = (ComponentIdentifier) o; + return Objects.equals(extensionName, that.extensionName) && Objects.equals(identifier, that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(extensionName, identifier); + } + + @Override + public String toString() { + return "ComponentIdentifier{" + + "extensionName='" + extensionName + '\'' + + ", identifier='" + identifier + '\'' + + '}'; + } +} diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/component/actionrow/MessageActionRow.java b/api/src/main/java/com/discordsrv/api/discord/entity/component/actionrow/MessageActionRow.java index 23f06ef3..3cc67048 100644 --- a/api/src/main/java/com/discordsrv/api/discord/entity/component/actionrow/MessageActionRow.java +++ b/api/src/main/java/com/discordsrv/api/discord/entity/component/actionrow/MessageActionRow.java @@ -30,7 +30,7 @@ import java.util.List; public class MessageActionRow implements ActionRow { - public static MessageActionRow message(MessageComponent... components) { + public static MessageActionRow of(MessageComponent... components) { if (components.length == 0) { throw new IllegalArgumentException("Must include at least one component"); } diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/component/Button.java b/api/src/main/java/com/discordsrv/api/discord/entity/component/impl/Button.java similarity index 74% rename from api/src/main/java/com/discordsrv/api/discord/entity/component/Button.java rename to api/src/main/java/com/discordsrv/api/discord/entity/component/impl/Button.java index 9c467df0..b34c7b23 100644 --- a/api/src/main/java/com/discordsrv/api/discord/entity/component/Button.java +++ b/api/src/main/java/com/discordsrv/api/discord/entity/component/impl/Button.java @@ -21,10 +21,12 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.entity.component; +package com.discordsrv.api.discord.entity.component.impl; -import com.discordsrv.api.discord.entity.guild.DiscordEmote; -import net.dv8tion.jda.api.entities.Emoji; +import com.discordsrv.api.discord.entity.component.ComponentIdentifier; +import com.discordsrv.api.discord.entity.component.MessageComponent; +import com.discordsrv.api.discord.entity.guild.DiscordCustomEmoji; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.interactions.components.ItemComponent; import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; import org.jetbrains.annotations.NotNull; @@ -35,7 +37,8 @@ import java.util.UUID; /** * A Discord button. - * @see #builder(Style) + * @see #builder(ComponentIdentifier, Style) + * @see #urlBuilder(String) */ public class Button implements MessageComponent { @@ -44,8 +47,19 @@ public class Button implements MessageComponent { * @param style the style of the button * @return a new button builder */ - public static Builder builder(@NotNull Button.Style style) { - return new Builder(style); + @NotNull + public static Builder builder(@NotNull ComponentIdentifier id, @NotNull Button.Style style) { + return new Builder(id.getDiscordIdentifier(), style); + } + + /** + * Creates a new Link button builder. + * @param url the link the button leads to + * @return a new button builder + */ + @NotNull + public static Builder urlBuilder(@NotNull String url) { + return new Builder(null, Style.LINK).setUrl(url); } private final Style buttonStyle; @@ -53,22 +67,20 @@ public class Button implements MessageComponent { private final String label; private final Emoji emoji; private final boolean disabled; - private final ClickHandler clickHandler; private Button( + String id, Style buttonStyle, String url, String label, Emoji emoji, - boolean disabled, - ClickHandler clickHandler + boolean disabled ) { this.buttonStyle = buttonStyle; - this.idOrUrl = buttonStyle == Style.LINK ? url : UUID.randomUUID().toString(); + this.idOrUrl = buttonStyle == Style.LINK ? url : id; this.label = label; this.emoji = emoji; this.disabled = disabled; - this.clickHandler = clickHandler; } @NotNull @@ -95,10 +107,6 @@ public class Button implements MessageComponent { return disabled; } - public ClickHandler getClickHandler() { - return clickHandler; - } - @Override public ItemComponent asJDA() { return net.dv8tion.jda.api.interactions.components.buttons.Button.of( @@ -111,22 +119,18 @@ public class Button implements MessageComponent { private static class Builder { + private final String id; private final Style style; private String url; private String label; private Emoji emoji; private boolean disabled; - private ClickHandler clickHandler; - private Builder(Style style) { + private Builder(String id, Style style) { + this.id = id; this.style = style; } - @NotNull - public Style getStyle() { - return style; - } - /** * Sets the url for this button, only works if the style is {@link Style#LINK}. * @@ -142,10 +146,6 @@ public class Button implements MessageComponent { return this; } - public String getUrl() { - return url; - } - /** * Sets the text shown on this button. * @param label the text @@ -157,10 +157,6 @@ public class Button implements MessageComponent { return this; } - public String getLabel() { - return label; - } - /** * Sets the emoji to show on this button. * @param unicodeEmoji the unicode code point for the emoji @@ -178,15 +174,11 @@ public class Button implements MessageComponent { * @return this builder, useful for chaining */ @NotNull - public Builder setEmoji(DiscordEmote emote) { - this.emoji = Emoji.fromEmote(emote.asJDA()); + public Builder setEmoji(DiscordCustomEmoji emote) { + this.emoji = Emoji.fromCustom(emote.asJDA()); return this; } - public Emoji getEmoji() { - return emoji; - } - /** * Set if this button is disabled or not. Default is {@code false}. * @param disabled if this button should be disabled @@ -198,28 +190,6 @@ public class Button implements MessageComponent { return this; } - public boolean isDisabled() { - return disabled; - } - - /** - * Sets the click handler for this button, does not work if the style is {@link Style#LINK}. - * @param clickHandler the click handler - * @return this builder, useful for chaining - */ - @NotNull - public Builder setClickHandler(ClickHandler clickHandler) { - if (style == Style.LINK) { - throw new IllegalStateException("Cannot set click handler for LINK type button, use setUrl instead"); - } - this.clickHandler = clickHandler; - return this; - } - - public ClickHandler getClickHandler() { - return clickHandler; - } - /** * Creates the button. * @return a new button @@ -229,12 +199,12 @@ public class Button implements MessageComponent { throw new IllegalStateException("No style set"); } return new Button( + id, style, style == Style.LINK ? url : UUID.randomUUID().toString(), label, emoji, - disabled, - clickHandler + disabled ); } } @@ -256,11 +226,4 @@ public class Button implements MessageComponent { return style; } } - - @FunctionalInterface - public interface ClickHandler { - - void onClick(Interaction interaction); - - } } diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/component/Modal.java b/api/src/main/java/com/discordsrv/api/discord/entity/component/impl/Modal.java similarity index 83% rename from api/src/main/java/com/discordsrv/api/discord/entity/component/Modal.java rename to api/src/main/java/com/discordsrv/api/discord/entity/component/impl/Modal.java index a7eb7123..8bd25ffb 100644 --- a/api/src/main/java/com/discordsrv/api/discord/entity/component/Modal.java +++ b/api/src/main/java/com/discordsrv/api/discord/entity/component/impl/Modal.java @@ -21,20 +21,22 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.entity.component; +package com.discordsrv.api.discord.entity.component.impl; import com.discordsrv.api.discord.entity.JDAEntity; +import com.discordsrv.api.discord.entity.component.ComponentIdentifier; import com.discordsrv.api.discord.entity.component.actionrow.ActionRow; import com.discordsrv.api.discord.entity.component.actionrow.ModalActionRow; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.stream.Collectors; /** * A Discord modal. - * @see #builder(String) + * @see #builder(ComponentIdentifier, String) */ public class Modal implements JDAEntity { @@ -43,16 +45,17 @@ public class Modal implements JDAEntity rows; - private Modal(String title, List rows) { - this.id = UUID.randomUUID().toString(); + private Modal(String id, String title, List rows) { + this.id = id; this.title = title; this.rows = rows; } @@ -79,10 +82,12 @@ public class Modal implements JDAEntity rows = new ArrayList<>(); - public Builder(String title) { + public Builder(String id, String title) { + this.id = id; this.title = title; } @@ -108,18 +113,12 @@ public class Modal implements JDAEntity getRows() { - return Collections.unmodifiableList(rows); - } - /** * Builds the modal. * @return a new modal */ public Modal build() { - return new Modal(title, rows); + return new Modal(id, title, rows); } } } diff --git a/api/src/main/java/com/discordsrv/api/discord/entity/component/SelectMenu.java b/api/src/main/java/com/discordsrv/api/discord/entity/component/impl/SelectMenu.java similarity index 82% rename from api/src/main/java/com/discordsrv/api/discord/entity/component/SelectMenu.java rename to api/src/main/java/com/discordsrv/api/discord/entity/component/impl/SelectMenu.java index 9e3b2445..e5505a06 100644 --- a/api/src/main/java/com/discordsrv/api/discord/entity/component/SelectMenu.java +++ b/api/src/main/java/com/discordsrv/api/discord/entity/component/impl/SelectMenu.java @@ -21,21 +21,22 @@ * SOFTWARE. */ -package com.discordsrv.api.discord.entity.component; +package com.discordsrv.api.discord.entity.component.impl; -import com.discordsrv.api.discord.entity.guild.DiscordEmote; -import net.dv8tion.jda.api.entities.Emoji; +import com.discordsrv.api.discord.entity.component.ComponentIdentifier; +import com.discordsrv.api.discord.entity.component.MessageComponent; +import com.discordsrv.api.discord.entity.guild.DiscordCustomEmoji; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.interactions.components.ItemComponent; import net.dv8tion.jda.api.interactions.components.selections.SelectOption; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; import javax.annotation.CheckReturnValue; import java.util.*; /** * A Discord selection menu. - * @see #builder() + * @see #builder(ComponentIdentifier) */ public class SelectMenu implements MessageComponent { @@ -43,8 +44,8 @@ public class SelectMenu implements MessageComponent { * Creates a selection menu builder. * @return a new builder */ - public static Builder builder() { - return new Builder(); + public static Builder builder(@NotNull ComponentIdentifier id) { + return new Builder(id.getDiscordIdentifier()); } private final String id; @@ -53,16 +54,14 @@ public class SelectMenu implements MessageComponent { private final String placeholder; private final int minValues; private final int maxValues; - private final SelectHandler handler; - private SelectMenu(List