From 2b00e59c9a79c82f76d904fe7b7770502920f8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Spie=C3=9F?= Date: Thu, 28 Sep 2023 21:54:06 +0200 Subject: [PATCH] Add default_values support (#2542) --- .../selections/EntitySelectMenu.java | 330 +++++++++++++++++- .../component/EntitySelectMenuImpl.java | 25 +- .../jda/interactions/SelectMenuTests.java | 97 +++++ 3 files changed, 445 insertions(+), 7 deletions(-) create mode 100644 src/test/java/net/dv8tion/jda/interactions/SelectMenuTests.java diff --git a/src/main/java/net/dv8tion/jda/api/interactions/components/selections/EntitySelectMenu.java b/src/main/java/net/dv8tion/jda/api/interactions/components/selections/EntitySelectMenu.java index 3d273572cd..d93f005a8c 100644 --- a/src/main/java/net/dv8tion/jda/api/interactions/components/selections/EntitySelectMenu.java +++ b/src/main/java/net/dv8tion/jda/api/interactions/components/selections/EntitySelectMenu.java @@ -16,18 +16,25 @@ package net.dv8tion.jda.api.interactions.components.selections; +import net.dv8tion.jda.api.entities.ISnowflake; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.UserSnowflake; import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; +import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionEvent; import net.dv8tion.jda.api.interactions.components.ActionComponent; import net.dv8tion.jda.api.interactions.components.Component; +import net.dv8tion.jda.api.utils.MiscUtil; +import net.dv8tion.jda.api.utils.data.DataObject; +import net.dv8tion.jda.api.utils.data.SerializableData; import net.dv8tion.jda.internal.interactions.component.EntitySelectMenuImpl; import net.dv8tion.jda.internal.utils.Checks; +import net.dv8tion.jda.internal.utils.EntityString; import net.dv8tion.jda.internal.utils.Helpers; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; -import java.util.Arrays; -import java.util.Collection; -import java.util.EnumSet; +import java.util.*; /** * Specialized {@link SelectMenu} for selecting Discord entities. @@ -108,6 +115,16 @@ default EntitySelectMenu withDisabled(boolean disabled) @Nonnull EnumSet getChannelTypes(); + /** + * Default selected values. + *
These are shown until the user customizes the selected values, + * which then fires a {@link EntitySelectInteractionEvent}. + * + * @return Immutable list of {@link DefaultValue default values} + */ + @Nonnull + List getDefaultValues(); + /** * Creates a new preconfigured {@link Builder} with the same settings used for this select menu. *
This can be useful to create an updated version of this menu without needing to rebuild it from scratch. @@ -193,6 +210,245 @@ enum SelectTarget CHANNEL } + /** + * Represents the default values used in {@link #getDefaultValues()}. + *
The value is {@link #getType() typed} correspondingly to the menu {@link EntitySelectMenu#getEntityTypes() entity types}. + * + *

The value is represented by the {@link #getId() ID}, corresponding to the entity of that ID. + */ + class DefaultValue implements ISnowflake, SerializableData + { + private final long id; + private final SelectTarget type; + + protected DefaultValue(long id, @Nonnull SelectTarget type) + { + this.id = id; + this.type = type; + } + + /** + * Parses the provided {@link DataObject} into the default value. + * + * @param object + * The serialized default value, with a valid type and id + * + * @throws IllegalArgumentException + * If the provided object is invalid or missing required keys + * + * @return Parsed default value + */ + @Nonnull + public static DefaultValue fromData(@Nonnull DataObject object) + { + Checks.notNull(object, "DataObject"); + long id = object.getUnsignedLong("id"); + switch (object.getString("type")) + { + case "role": + return role(id); + case "user": + return user(id); + case "channel": + return channel(id); + } + throw new IllegalArgumentException("Unknown value type '" + object.getString("type", null) + "'"); + } + + /** + * Creates a default value of type {@link SelectTarget#USER} for the provided user. + * + * @param user + * The corresponding user + * + * @throws IllegalArgumentException + * If null is provided + * + * @return The default value + */ + @Nonnull + public static DefaultValue from(@Nonnull UserSnowflake user) + { + Checks.notNull(user, "User"); + return user(user.getIdLong()); + } + + /** + * Creates a default value of type {@link SelectTarget#ROLE} for the provided role. + * + * @param role + * The corresponding role + * + * @throws IllegalArgumentException + * If null is provided + * + * @return The default value + */ + @Nonnull + public static DefaultValue from(@Nonnull Role role) + { + Checks.notNull(role, "Role"); + return role(role.getIdLong()); + } + + /** + * Creates a default value of type {@link SelectTarget#CHANNEL} for the provided channel. + * + * @param channel + * The corresponding channel + * + * @throws IllegalArgumentException + * If null is provided + * + * @return The default value + */ + @Nonnull + public static DefaultValue from(@Nonnull GuildChannel channel) + { + Checks.notNull(channel, "Channel"); + return channel(channel.getIdLong()); + } + + /** + * Creates a default value of type {@link SelectTarget#ROLE} with the provided id. + * + * @param id + * The role id + * + * @return The default value + */ + @Nonnull + public static DefaultValue role(long id) + { + return new DefaultValue(id, SelectTarget.ROLE); + } + + /** + * Creates a default value of type {@link SelectTarget#ROLE} with the provided id. + * + * @param id + * The role id + * + * @throws IllegalArgumentException + * If the provided id is not a valid snowflake + * + * @return The default value + */ + @Nonnull + public static DefaultValue role(@Nonnull String id) + { + return new DefaultValue(MiscUtil.parseSnowflake(id), SelectTarget.ROLE); + } + + /** + * Creates a default value of type {@link SelectTarget#USER} with the provided id. + * + * @param id + * The role id + * + * @return The default value + */ + @Nonnull + public static DefaultValue user(long id) + { + return new DefaultValue(id, SelectTarget.USER); + } + + /** + * Creates a default value of type {@link SelectTarget#USER} with the provided id. + * + * @param id + * The role id + * + * @throws IllegalArgumentException + * If the provided id is not a valid snowflake + * + * @return The default value + */ + @Nonnull + public static DefaultValue user(@Nonnull String id) + { + return new DefaultValue(MiscUtil.parseSnowflake(id), SelectTarget.USER); + } + + /** + * Creates a default value of type {@link SelectTarget#CHANNEL} with the provided id. + * + * @param id + * The role id + * + * @return The default value + */ + @Nonnull + public static DefaultValue channel(long id) + { + return new DefaultValue(id, SelectTarget.CHANNEL); + } + + /** + * Creates a default value of type {@link SelectTarget#CHANNEL} with the provided id. + * + * @param id + * The role id + * + * @throws IllegalArgumentException + * If the provided id is not a valid snowflake + * + * @return The default value + */ + @Nonnull + public static DefaultValue channel(@Nonnull String id) + { + return new DefaultValue(MiscUtil.parseSnowflake(id), SelectTarget.CHANNEL); + } + + @Override + public long getIdLong() + { + return id; + } + + @Nonnull + public SelectTarget getType() + { + return type; + } + + @Nonnull + @Override + public DataObject toData() + { + return DataObject.empty() + .put("type", type.name().toLowerCase(Locale.ROOT)) + .put("id", getId()); + } + + @Override + public int hashCode() + { + return Objects.hash(type, id); + } + + @Override + public boolean equals(Object obj) + { + if (obj == this) + return true; + if (!(obj instanceof DefaultValue)) + return false; + DefaultValue other = (DefaultValue) obj; + return id == other.id && type == other.type; + } + + @Override + public String toString() + { + return new EntityString(this) + .setType(type) + .toString(); + } + } + /** * A preconfigured builder for the creation of entity select menus. */ @@ -200,6 +456,7 @@ class Builder extends SelectMenu.Builder { protected Component.Type componentType; protected EnumSet channelTypes = EnumSet.noneOf(ChannelType.class); + protected List defaultValues = new ArrayList<>(); protected Builder(@Nonnull String customId) { @@ -309,6 +566,70 @@ public Builder setChannelTypes(@Nonnull ChannelType... types) return setChannelTypes(Arrays.asList(types)); } + /** + * The {@link #getDefaultValues() default values} that will be shown to the user. + * + * @param values + * The default values (up to {@value #OPTIONS_MAX_AMOUNT}) + * + * @throws IllegalArgumentException + * If null is provided, more than {@value #OPTIONS_MAX_AMOUNT} values are provided, + * or any of the value types is incompatible with the configured {@link #setEntityTypes(Collection) entity types}. + * + * @return The current Builder instance + */ + @Nonnull + public Builder setDefaultValues(@Nonnull DefaultValue... values) + { + Checks.noneNull(values, "Default Values"); + return setDefaultValues(Arrays.asList(values)); + } + + /** + * The {@link #getDefaultValues() default values} that will be shown to the user. + * + * @param values + * The default values (up to {@value #OPTIONS_MAX_AMOUNT}) + * + * @throws IllegalArgumentException + * If null is provided, more than {@value #OPTIONS_MAX_AMOUNT} values are provided, + * or any of the value types is incompatible with the configured {@link #setEntityTypes(Collection) entity types}. + * + * @return The current Builder instance + */ + @Nonnull + public Builder setDefaultValues(@Nonnull Collection values) + { + Checks.noneNull(values, "Default Values"); + Checks.check(values.size() <= SelectMenu.OPTIONS_MAX_AMOUNT, "Cannot add more than %d default values to a select menu!", SelectMenu.OPTIONS_MAX_AMOUNT); + + for (DefaultValue value : values) + { + SelectTarget type = value.getType(); + String error = "The select menu supports types %s, but provided default value has type SelectTarget.%s!"; + + switch (componentType) + { + case ROLE_SELECT: + Checks.check(type == SelectTarget.ROLE, error, "SelectTarget.ROLE", type); + break; + case USER_SELECT: + Checks.check(type == SelectTarget.USER, error, "SelectTarget.USER", type); + break; + case CHANNEL_SELECT: + Checks.check(type == SelectTarget.CHANNEL, error, "SelectTarget.CHANNEL", type); + break; + case MENTIONABLE_SELECT: + Checks.check(type == SelectTarget.ROLE || type == SelectTarget.USER, error, "SelectTarget.ROLE and SelectTarget.USER", type); + break; + } + } + + this.defaultValues.clear(); + this.defaultValues.addAll(values); + return this; + } + /** * Creates a new {@link EntitySelectMenu} instance if all requirements are satisfied. * @@ -323,7 +644,8 @@ public EntitySelectMenu build() { Checks.check(minValues <= maxValues, "Min values cannot be greater than max values!"); EnumSet channelTypes = componentType == Type.CHANNEL_SELECT ? this.channelTypes : EnumSet.noneOf(ChannelType.class); - return new EntitySelectMenuImpl(customId, placeholder, minValues, maxValues, disabled, componentType, channelTypes); + List defaultValues = new ArrayList<>(this.defaultValues); + return new EntitySelectMenuImpl(customId, placeholder, minValues, maxValues, disabled, componentType, channelTypes, defaultValues); } } } diff --git a/src/main/java/net/dv8tion/jda/internal/interactions/component/EntitySelectMenuImpl.java b/src/main/java/net/dv8tion/jda/internal/interactions/component/EntitySelectMenuImpl.java index 5da192976c..a760810b6e 100644 --- a/src/main/java/net/dv8tion/jda/internal/interactions/component/EntitySelectMenuImpl.java +++ b/src/main/java/net/dv8tion/jda/internal/interactions/component/EntitySelectMenuImpl.java @@ -25,7 +25,9 @@ import org.jetbrains.annotations.NotNull; import javax.annotation.Nonnull; +import java.util.Collections; import java.util.EnumSet; +import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -33,6 +35,7 @@ public class EntitySelectMenuImpl extends SelectMenuImpl implements EntitySelect { protected final Component.Type type; protected final EnumSet channelTypes; + protected final List defaultValues; public EntitySelectMenuImpl(DataObject data) { @@ -41,13 +44,19 @@ public EntitySelectMenuImpl(DataObject data) this.channelTypes = Helpers.copyEnumSet(ChannelType.class, data.optArray("channel_types").map( arr -> arr.stream(DataArray::getInt).map(ChannelType::fromId).collect(Collectors.toList()) ).orElse(null)); + this.defaultValues = data.optArray("default_values").map(array -> + array.stream(DataArray::getObject) + .map(DefaultValue::fromData) + .collect(Helpers.toUnmodifiableList()) + ).orElse(Collections.emptyList()); } - public EntitySelectMenuImpl(String id, String placeholder, int minValues, int maxValues, boolean disabled, Type type, EnumSet channelTypes) + public EntitySelectMenuImpl(String id, String placeholder, int minValues, int maxValues, boolean disabled, Type type, EnumSet channelTypes, List defaultValues) { super(id, placeholder, minValues, maxValues, disabled); this.type = type; this.channelTypes = channelTypes; + this.defaultValues = defaultValues; } @Nonnull @@ -83,6 +92,13 @@ public EnumSet getChannelTypes() return channelTypes; } + @Nonnull + @Override + public List getDefaultValues() + { + return defaultValues; + } + @NotNull @Override public DataObject toData() @@ -90,13 +106,15 @@ public DataObject toData() DataObject json = super.toData().put("type", type.getKey()); if (type == Type.CHANNEL_SELECT && !channelTypes.isEmpty()) json.put("channel_types", DataArray.fromCollection(channelTypes.stream().map(ChannelType::getId).collect(Collectors.toList()))); + if (!defaultValues.isEmpty()) + json.put("default_values", DataArray.fromCollection(defaultValues)); return json; } @Override public int hashCode() { - return Objects.hash(id, placeholder, minValues, maxValues, disabled, type, channelTypes); + return Objects.hash(id, placeholder, minValues, maxValues, disabled, type, channelTypes, defaultValues); } @Override @@ -113,6 +131,7 @@ public boolean equals(Object obj) && maxValues == other.getMaxValues() && disabled == other.isDisabled() && type == other.getType() - && channelTypes.equals(other.getChannelTypes()); + && channelTypes.equals(other.getChannelTypes()) + && defaultValues.equals(other.getDefaultValues()); } } diff --git a/src/test/java/net/dv8tion/jda/interactions/SelectMenuTests.java b/src/test/java/net/dv8tion/jda/interactions/SelectMenuTests.java new file mode 100644 index 0000000000..670c7dd30b --- /dev/null +++ b/src/test/java/net/dv8tion/jda/interactions/SelectMenuTests.java @@ -0,0 +1,97 @@ +/* + * 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.interactions; + +import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu; +import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.Builder; +import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.DefaultValue; +import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu.SelectTarget; +import net.dv8tion.jda.api.utils.data.DataObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class SelectMenuTests +{ + @Test + public void testEntitySelectDefaultValueValid() + { + Builder builder = EntitySelectMenu.create("customid", SelectTarget.ROLE); + builder.setDefaultValues(DefaultValue.role("1234")); + + EntitySelectMenu menu = builder.build(); + DataObject value = menu.toData().getArray("default_values").getObject(0); + + Assertions.assertEquals(Arrays.asList(DefaultValue.role("1234")), menu.getDefaultValues()); + Assertions.assertEquals("role", value.getString("type")); + Assertions.assertEquals("1234", value.getString("id")); + + builder = EntitySelectMenu.create("customid", SelectTarget.USER); + builder.setDefaultValues(DefaultValue.user("1234")); + + menu = builder.build(); + value = menu.toData().getArray("default_values").getObject(0); + + Assertions.assertEquals(Arrays.asList(DefaultValue.user("1234")), menu.getDefaultValues()); + Assertions.assertEquals("user", value.getString("type")); + Assertions.assertEquals("1234", value.getString("id")); + + builder = EntitySelectMenu.create("customid", SelectTarget.CHANNEL); + builder.setDefaultValues(DefaultValue.channel("1234")); + + menu = builder.build(); + value = menu.toData().getArray("default_values").getObject(0); + + Assertions.assertEquals(Arrays.asList(DefaultValue.channel("1234")), menu.getDefaultValues()); + Assertions.assertEquals("channel", value.getString("type")); + Assertions.assertEquals("1234", value.getString("id")); + } + + @Test + public void testEntitySelectDefaultValueInvalid() + { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + Builder builder = EntitySelectMenu.create("customid", SelectTarget.ROLE); + builder.setDefaultValues(DefaultValue.user("1234")); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + Builder builder = EntitySelectMenu.create("customid", SelectTarget.ROLE); + builder.setDefaultValues(DefaultValue.channel("1234")); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + Builder builder = EntitySelectMenu.create("customid", SelectTarget.ROLE, SelectTarget.USER); + builder.setDefaultValues(DefaultValue.channel("1234")); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + Builder builder = EntitySelectMenu.create("customid", SelectTarget.USER); + builder.setDefaultValues(DefaultValue.channel("1234")); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + Builder builder = EntitySelectMenu.create("customid", SelectTarget.USER); + builder.setDefaultValues(DefaultValue.role("1234")); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + Builder builder = EntitySelectMenu.create("customid", SelectTarget.CHANNEL); + builder.setDefaultValues(DefaultValue.user("1234")); + }); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + Builder builder = EntitySelectMenu.create("customid", SelectTarget.CHANNEL); + builder.setDefaultValues(DefaultValue.role("1234")); + }); + } +}