net.dv8tion.jda.api.interactions.commands.build.SubcommandData Maven / Gradle / Ivy
Show all versions of JDA Show documentation
/*
* Copyright 2015 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors
*
* 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 net.dv8tion.jda.api.interactions.commands.build;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.interactions.DiscordLocale;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.localization.LocalizationMap;
import net.dv8tion.jda.api.utils.data.DataArray;
import net.dv8tion.jda.api.utils.data.DataObject;
import net.dv8tion.jda.api.utils.data.SerializableData;
import net.dv8tion.jda.internal.utils.Checks;
import net.dv8tion.jda.internal.utils.localization.LocalizationUtils;
import javax.annotation.Nonnull;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Builder for a Slash-Command subcommand.
*/
public class SubcommandData implements SerializableData
{
protected final List options = new ArrayList<>(CommandData.MAX_OPTIONS);
protected String name, description;
private final LocalizationMap nameLocalizations = new LocalizationMap(this::checkName);
private final LocalizationMap descriptionLocalizations = new LocalizationMap(this::checkDescription);
private boolean allowRequired = true;
/**
* Create a subcommand builder.
*
* @param name
* The subcommand name, 1-32 lowercase alphanumeric characters
* @param description
* The subcommand description, 1-100 characters
*
* @throws IllegalArgumentException
* If any of the following requirements are not met
*
* - The name must be lowercase alphanumeric (with dash), 1-32 characters long
* - The description must be 1-100 characters long
*
*/
public SubcommandData(@Nonnull String name, @Nonnull String description)
{
setName(name);
setDescription(description);
}
protected void checkName(@Nonnull String name)
{
Checks.inRange(name, 1, 32, "Name");
Checks.matches(name, Checks.ALPHANUMERIC_WITH_DASH, "Name");
Checks.isLowercase(name, "Name");
}
protected void checkDescription(@Nonnull String description)
{
Checks.notEmpty(description, "Description");
Checks.notLonger(description, 100, "Description");
}
/**
* Configure the name
*
* @param name
* The lowercase alphanumeric (with dash) name, 1-32 characters
*
* @throws IllegalArgumentException
* If the name is null, not alphanumeric, or not between 1-32 characters
*
* @return The SubcommandData instance, for chaining
*/
@Nonnull
public SubcommandData setName(@Nonnull String name)
{
checkName(name);
this.name = name;
return this;
}
/**
* Sets a {@link DiscordLocale language-specific} localization of this subcommand's name.
*
* @param locale
* The locale to associate the translated name with
*
* @param name
* The translated name to put
*
* @throws IllegalArgumentException
*
* - If the locale is null
* - If the name is null
* - If the locale is {@link DiscordLocale#UNKNOWN}
* - If the name does not pass the corresponding {@link #setName(String) name check}
*
*
* @return This builder instance, for chaining
*/
@Nonnull
public SubcommandData setNameLocalization(@Nonnull DiscordLocale locale, @Nonnull String name)
{
//Checks are done in LocalizationMap
nameLocalizations.setTranslation(locale, name);
return this;
}
/**
* Sets multiple {@link DiscordLocale language-specific} localizations of this subcommand's name.
*
* @param map
* The map from which to transfer the translated names
*
* @throws IllegalArgumentException
*
* - If the map is null
* - If the map contains an {@link DiscordLocale#UNKNOWN} key
* - If the map contains a name which does not pass the corresponding {@link #setName(String) name check}
*
*
* @return This builder instance, for chaining
*/
@Nonnull
public SubcommandData setNameLocalizations(@Nonnull Map map)
{
//Checks are done in LocalizationMap
nameLocalizations.setTranslations(map);
return this;
}
/**
* Configure the description
*
* @param description
* The description, 1-100 characters
*
* @throws IllegalArgumentException
* If the name is null or not between 1-100 characters
*
* @return The SubcommandData instance, for chaining
*/
@Nonnull
public SubcommandData setDescription(@Nonnull String description)
{
checkDescription(description);
this.description = description;
return this;
}
/**
* Sets a {@link DiscordLocale language-specific} localization of this subcommand's description.
*
* @param locale
* The locale to associate the translated description with
*
* @param description
* The translated description to put
*
* @throws IllegalArgumentException
*
* - If the locale is null
* - If the description is null
* - If the locale is {@link DiscordLocale#UNKNOWN}
* - If the description does not pass the corresponding {@link #setDescription(String) description check}
*
*
* @return This builder instance, for chaining
*/
@Nonnull
public SubcommandData setDescriptionLocalization(@Nonnull DiscordLocale locale, @Nonnull String description)
{
//Checks are done in LocalizationMap
descriptionLocalizations.setTranslation(locale, description);
return this;
}
/**
* Sets multiple {@link DiscordLocale language-specific} localizations of this subcommand's description.
*
* @param map
* The map from which to transfer the translated descriptions
*
* @throws IllegalArgumentException
*
* - If the map is null
* - If the map contains an {@link DiscordLocale#UNKNOWN} key
* - If the map contains a description which does not pass the corresponding {@link #setDescription(String) description check}
*
*
* @return This builder instance, for chaining
*/
@Nonnull
public SubcommandData setDescriptionLocalizations(@Nonnull Map map)
{
//Checks are done in LocalizationMap
descriptionLocalizations.setTranslations(map);
return this;
}
/**
* Removes all options that evaluate to {@code true} under the provided {@code condition}.
*
* Example: Remove all options
*
{@code
* command.removeOptions(option -> true);
* }
* Example: Remove all options that are required
*
{@code
* command.removeOptions(option -> option.isRequired());
* }
*
* @param condition
* The removal condition (must not throw)
*
* @throws IllegalArgumentException
* If the condition is null
*
* @return True, if any options were removed
*/
public boolean removeOptions(@Nonnull Predicate super OptionData> condition)
{
Checks.notNull(condition, "Condition");
return options.removeIf(condition);
}
/**
* Removes options by the provided name.
*
* @param name
* The case-sensitive option name
*
* @return True, if any options were removed
*/
public boolean removeOptionByName(@Nonnull String name)
{
return removeOptions(option -> option.getName().equals(name));
}
/**
* Adds up to {@value CommandData#MAX_OPTIONS} options to this subcommand.
*
* Required options must be added before non-required options!
*
* @param options
* The {@link OptionData options} to add
*
* @throws IllegalArgumentException
*
* - If this option is required and you already added a non-required option.
* - If more than {@value CommandData#MAX_OPTIONS} options are provided.
* - If the option name is not unique
* - If null is provided
*
*
* @return The SubcommandData instance, for chaining
*/
@Nonnull
public SubcommandData addOptions(@Nonnull OptionData... options)
{
Checks.noneNull(options, "Option");
Checks.check(options.length + this.options.size() <= CommandData.MAX_OPTIONS, "Cannot have more than %d options for a subcommand!", CommandData.MAX_OPTIONS);
boolean allowRequired = this.allowRequired;
for (OptionData option : options)
{
Checks.check(option.getType() != OptionType.SUB_COMMAND, "Cannot add a subcommand to a subcommand!");
Checks.check(option.getType() != OptionType.SUB_COMMAND_GROUP, "Cannot add a subcommand group to a subcommand!");
Checks.check(allowRequired || !option.isRequired(), "Cannot add required options after non-required options!");
allowRequired = option.isRequired(); // prevent adding required options after non-required options
}
Checks.checkUnique(Stream.concat(getOptions().stream(), Arrays.stream(options)).map(OptionData::getName),
"Cannot have multiple options with the same name. Name: \"%s\" appeared %d times!",
(count, value) -> new Object[]{ value, count });
this.allowRequired = allowRequired;
this.options.addAll(Arrays.asList(options));
return this;
}
/**
* Adds up to {@value CommandData#MAX_OPTIONS} options to this subcommand.
*
* Required options must be added before non-required options!
*
* @param options
* The {@link OptionData options} to add
*
* @throws IllegalArgumentException
*
* - If this option is required and you already added a non-required option.
* - If more than {@value CommandData#MAX_OPTIONS} options are provided.
* - If the option name is not unique
* - If null is provided
*
*
* @return The SubcommandData instance, for chaining
*/
@Nonnull
public SubcommandData addOptions(@Nonnull Collection extends OptionData> options)
{
Checks.noneNull(options, "Options");
return addOptions(options.toArray(new OptionData[0]));
}
/**
* Adds an option to this subcommand.
*
* Required options must be added before non-required options!
*
* @param type
* The {@link OptionType}
* @param name
* The lowercase option name, 1-{@value OptionData#MAX_NAME_LENGTH} characters
* @param description
* The option description, 1-{@value OptionData#MAX_DESCRIPTION_LENGTH} characters
* @param required
* Whether this option is required (See {@link OptionData#setRequired(boolean)})
* @param autoComplete
* Whether this option supports auto-complete via {@link CommandAutoCompleteInteractionEvent},
* only supported for option types which {@link OptionType#canSupportChoices() support choices}
*
* @throws IllegalArgumentException
*
* - If this option is required and you already added a non-required option.
* - If the provided option type does not support auto-complete
* - If more than {@value CommandData#MAX_OPTIONS} options are provided.
* - If the option name is not unique
* - If null is provided
*
*
* @return The SubcommandData instance, for chaining
*/
@Nonnull
public SubcommandData addOption(@Nonnull OptionType type, @Nonnull String name, @Nonnull String description, boolean required, boolean autoComplete)
{
return addOptions(new OptionData(type, name, description)
.setRequired(required)
.setAutoComplete(autoComplete));
}
/**
* Adds an option to this subcommand.
*
* Required options must be added before non-required options!
*
* @param type
* The {@link OptionType}
* @param name
* The lowercase option name, 1-{@value OptionData#MAX_NAME_LENGTH} characters
* @param description
* The option description, 1-{@value OptionData#MAX_DESCRIPTION_LENGTH} characters
* @param required
* Whether this option is required (See {@link OptionData#setRequired(boolean)})
*
* @throws IllegalArgumentException
*
* - If this option is required and you already added a non-required option.
* - If more than {@value CommandData#MAX_OPTIONS} options are provided.
* - If the option name is not unique
* - If null is provided
*
*
* @return The SubcommandData instance, for chaining
*/
@Nonnull
public SubcommandData addOption(@Nonnull OptionType type, @Nonnull String name, @Nonnull String description, boolean required)
{
return addOption(type, name, description, required, false);
}
/**
* Adds an option to this subcommand.
*
The option is set to be non-required! You can use {@link #addOption(OptionType, String, String, boolean)} to add a required option instead.
*
* Required options must be added before non-required options!
*
* @param type
* The {@link OptionType}
* @param name
* The lowercase option name, 1-{@value OptionData#MAX_NAME_LENGTH} characters
* @param description
* The option description, 1-{@value OptionData#MAX_DESCRIPTION_LENGTH} characters
*
* @throws IllegalArgumentException
*
* - If this option is required and you already added a non-required option.
* - If more than {@value CommandData#MAX_OPTIONS} options are provided.
* - If the option name is not unique
* - If null is provided
*
*
* @return The SubcommandData instance, for chaining
*/
@Nonnull
public SubcommandData addOption(@Nonnull OptionType type, @Nonnull String name, @Nonnull String description)
{
return addOption(type, name, description, false);
}
/**
* The options for this command.
*
* @return Immutable list of {@link OptionData}
*/
@Nonnull
public List getOptions()
{
return Collections.unmodifiableList(options);
}
/**
* The configured name
*
* @return The name
*/
@Nonnull
public String getName()
{
return name;
}
/**
* The localizations of this subcommand's name for {@link DiscordLocale various languages}.
*
* @return The {@link LocalizationMap} containing the mapping from {@link DiscordLocale} to the localized name
*/
@Nonnull
public LocalizationMap getNameLocalizations()
{
return nameLocalizations;
}
/**
* The configured description
*
* @return The description
*/
@Nonnull
public String getDescription()
{
return description;
}
/**
* The localizations of this subcommand's description for {@link DiscordLocale various languages}.
*
* @return The {@link LocalizationMap} containing the mapping from {@link DiscordLocale} to the localized description
*/
@Nonnull
public LocalizationMap getDescriptionLocalizations()
{
return descriptionLocalizations;
}
@Nonnull
@Override
public DataObject toData()
{
return DataObject.empty()
.put("type", OptionType.SUB_COMMAND.getKey())
.put("name", name)
.put("name_localizations", nameLocalizations)
.put("description", description)
.put("description_localizations", descriptionLocalizations)
.put("options", DataArray.fromCollection(options));
}
/**
* Parses the provided serialization back into an SubcommandData instance.
*
This is the reverse function for {@link #toData()}.
*
* @param json
* The serialized {@link DataObject} representing the subcommand
*
* @throws net.dv8tion.jda.api.exceptions.ParsingException
* If the serialized object is missing required fields
* @throws IllegalArgumentException
* If any of the values are failing the respective checks such as length
*
* @return The parsed SubcommandData instance, which can be further configured through setters
*/
@Nonnull
public static SubcommandData fromData(@Nonnull DataObject json)
{
String name = json.getString("name");
String description = json.getString("description");
SubcommandData sub = new SubcommandData(name, description);
json.optArray("options").ifPresent(arr ->
arr.stream(DataArray::getObject)
.map(OptionData::fromData)
.forEach(sub::addOptions)
);
sub.setNameLocalizations(LocalizationUtils.mapFromProperty(json, "name_localizations"));
sub.setDescriptionLocalizations(LocalizationUtils.mapFromProperty(json, "description_localizations"));
return sub;
}
/**
* Converts the provided {@link Command.Subcommand} into a SubCommandData instance.
*
* @param subcommand
* The subcommand to convert
*
* @throws IllegalArgumentException
* If null is provided or the subcommand has illegal configuration
*
* @return An instance of SubCommandData
*/
@Nonnull
public static SubcommandData fromSubcommand(@Nonnull Command.Subcommand subcommand)
{
Checks.notNull(subcommand, "Subcommand");
SubcommandData data = new SubcommandData(subcommand.getName(), subcommand.getDescription());
data.setNameLocalizations(subcommand.getNameLocalizations().toMap());
data.setDescriptionLocalizations(subcommand.getDescriptionLocalizations().toMap());
subcommand.getOptions()
.stream()
.map(OptionData::fromOption)
.forEach(data::addOptions);
return data;
}
}