diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/parameters/resolvers/ModalParameterResolver.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/parameters/resolvers/ModalParameterResolver.kt index fabeb010f..cb8ff9469 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/parameters/resolvers/ModalParameterResolver.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/parameters/resolvers/ModalParameterResolver.kt @@ -6,6 +6,9 @@ import io.github.freya022.botcommands.api.modals.annotations.ModalInput import io.github.freya022.botcommands.api.modals.options.ModalOption import io.github.freya022.botcommands.api.parameters.ParameterResolver import net.dv8tion.jda.api.components.attachmentupload.AttachmentUpload +import net.dv8tion.jda.api.components.checkbox.Checkbox +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup +import net.dv8tion.jda.api.components.radiogroup.RadioGroup import net.dv8tion.jda.api.components.selections.EntitySelectMenu import net.dv8tion.jda.api.components.selections.StringSelectMenu import net.dv8tion.jda.api.components.textinput.TextInput @@ -21,11 +24,36 @@ import kotlin.reflect.KType * Needs to be implemented alongside a [ParameterResolver] subclass. * * ### Types supported by default - * - [TextInput] : `String` - * - [StringSelectMenu] : `List`, `String` - * - [EntitySelectMenu] : [Mentions], `T` and `List` where `T` is one of: + * **Note:** For `null` to be supported, the parameter must be explicitly nullable. + * + * #### [TextInput] + * - `String` (can be empty, supports `null` when empty) + * + * #### [StringSelectMenu] + * - `String` when a single value can be selected (supports `null` when none selected) + * - `List` (can be empty) + * + * #### [EntitySelectMenu] + * - [Mentions] + * - `T` (supports `null` when none selected) + * - `List` (can be empty) + * + * Where `T` is one of: * [IMentionable], [Role], [User], [InputUser], [Member], [GuildChannel] - * - [AttachmentUpload] : `List` of [Message.Attachment], [Message.Attachment] + * + * #### [AttachmentUpload] + * - `List` of [Message.Attachment] (can be empty) + * - [Message.Attachment] (supports `null` when none selected) + * + * #### [RadioGroup] + * - `String` (supports `null` when none selected) + * + * #### [CheckboxGroup] + * - `List` (can be empty) + * - `String` when a single value can be selected (supports `null` when none selected) + * + * #### [Checkbox] + * - (primitive) `Boolean` * * @param T Type of the implementation * @param R Type of the returned resolved objects diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt index c5dbbea30..286b21db5 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalHandlerInfo.kt @@ -105,7 +105,7 @@ internal class ModalHandlerInfo internal constructor( option.resolver.resolveSuspend(option, event, modalMapping).also { obj -> // Technically not required, but provides additional info requireUser(obj != null || option.isOptionalOrNullable) { - "The parameter '${option.declaredName}' from $modalMapping and value '${modalMapping.valueAsString}' is required but could not be resolved into a ${option.type.simpleNestedName}" + "The parameter '${option.declaredName}' from $modalMapping and value '${modalMapping.valueAsString}' is required but was resolved to null" } } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/resolvers/ModalBooleanResolver.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/resolvers/ModalBooleanResolver.kt new file mode 100644 index 000000000..a0a189598 --- /dev/null +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/resolvers/ModalBooleanResolver.kt @@ -0,0 +1,20 @@ +package io.github.freya022.botcommands.internal.modals.resolvers + +import io.github.freya022.botcommands.api.core.service.annotations.Resolver +import io.github.freya022.botcommands.api.modals.ModalEvent +import io.github.freya022.botcommands.api.modals.options.ModalOption +import io.github.freya022.botcommands.api.parameters.ClassParameterResolver +import io.github.freya022.botcommands.api.parameters.resolvers.ModalParameterResolver +import net.dv8tion.jda.api.interactions.modals.ModalMapping + +@Resolver +internal object ModalBooleanResolver : + ClassParameterResolver(Boolean::class), + ModalParameterResolver { + + override suspend fun resolveSuspend( + option: ModalOption, + event: ModalEvent, + modalMapping: ModalMapping, + ): Boolean = modalMapping.asBoolean +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/resolvers/ModalStringResolver.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/resolvers/ModalStringResolver.kt index 8a8db8241..e74e5f54f 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/resolvers/ModalStringResolver.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/resolvers/ModalStringResolver.kt @@ -5,6 +5,8 @@ import io.github.freya022.botcommands.api.modals.ModalEvent import io.github.freya022.botcommands.api.modals.options.ModalOption import io.github.freya022.botcommands.api.parameters.ClassParameterResolver import io.github.freya022.botcommands.api.parameters.resolvers.ModalParameterResolver +import io.github.freya022.botcommands.internal.utils.ReflectionUtils.function +import io.github.freya022.botcommands.internal.utils.throwArgument import net.dv8tion.jda.api.components.Component import net.dv8tion.jda.api.interactions.modals.ModalMapping @@ -19,13 +21,17 @@ internal object ModalStringResolver : modalMapping: ModalMapping, ): String? { return when (modalMapping.type) { - Component.Type.STRING_SELECT -> { + Component.Type.STRING_SELECT, Component.Type.CHECKBOX_GROUP -> { val values = modalMapping.asStringList - if (values.size > 1) - error("Cannot get a String from a string select menu with more than a single value") + if (values.size > 1) { + throwArgument( + option.kParameter.function, + "Cannot get a String from a ${modalMapping.type} with more than a single value" + ) + } values.firstOrNull() } - Component.Type.TEXT_INPUT -> modalMapping.asString + Component.Type.TEXT_INPUT, Component.Type.RADIO_GROUP -> modalMapping.asOptionalString else -> error("Cannot get a String from a ${modalMapping.type} input") } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/utils/ModalMappings.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/utils/ModalMappings.kt index d26bdb7fc..190601aab 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/utils/ModalMappings.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/utils/ModalMappings.kt @@ -9,9 +9,10 @@ internal val ModalInteractionEvent.allValuesAsString: String internal val ModalMapping.valueAsString: String get() = when (type) { - STRING_SELECT -> asStringList.toString() - TEXT_INPUT -> asString + STRING_SELECT, CHECKBOX_GROUP -> asStringList.toString() + TEXT_INPUT, RADIO_GROUP -> asOptionalString.toString() CHANNEL_SELECT, ROLE_SELECT, USER_SELECT, MENTIONABLE_SELECT -> asLongList.toString() FILE_UPLOAD -> asAttachmentList.map { "${it.fileName} (${it.contentType}, ${it.size} B)" }.toString() + CHECKBOX -> asBoolean.toString() else -> toString() } diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/modals/ModalInputResolverTests.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/modals/ModalInputResolverTests.kt index 035da2d1d..d23abe1c5 100644 --- a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/modals/ModalInputResolverTests.kt +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/modals/ModalInputResolverTests.kt @@ -60,7 +60,7 @@ object ModalInputResolverTests { @MethodSource("modalInputs") @ParameterizedTest - suspend fun `Modal input parameter can be resolved`(index: Int, type: Component.Type, expected: Any?) { + suspend fun `Modal input parameter can be resolved`(index: Int, type: Component.Type, getter: (ModalMapping) -> R, value: R, expected: Any?) { val serviceContainer = mockk { every { getServiceNamesForAnnotation(Resolver::class) } returns listOf( "modalMentionsResolver", @@ -68,6 +68,7 @@ object ModalInputResolverTests { "modalStringListResolver", "modalAttachmentResolver", "modalAttachmentListResolver", + "modalBooleanResolver", ) every { findAnnotationOnService("modalMentionsResolver", Resolver::class) } returns Resolver(0) @@ -75,28 +76,30 @@ object ModalInputResolverTests { every { findAnnotationOnService("modalStringListResolver", Resolver::class) } returns Resolver(0) every { findAnnotationOnService("modalAttachmentResolver", Resolver::class) } returns Resolver(0) every { findAnnotationOnService("modalAttachmentListResolver", Resolver::class) } returns Resolver(0) + every { findAnnotationOnService("modalBooleanResolver", Resolver::class) } returns Resolver(0) every { getService("modalMentionsResolver", ParameterResolver::class) } returns ModalMentionsResolver every { getService("modalStringResolver", ParameterResolver::class) } returns ModalStringResolver every { getService("modalStringListResolver", ParameterResolver::class) } returns ModalStringListResolver every { getService("modalAttachmentResolver", ParameterResolver::class) } returns ModalAttachmentResolver every { getService("modalAttachmentListResolver", ParameterResolver::class) } returns ModalAttachmentListResolver + every { getService("modalBooleanResolver", ParameterResolver::class) } returns ModalBooleanResolver } val resolvers = ResolverContainer(serviceContainer, listOf(ModalIMentionableResolverFactory)) - val request = ResolverRequest(ParameterWrapper(::userFunc.valueParameters[index])) + val parameter = ::userFunc.valueParameters[index] + val request = ResolverRequest(ParameterWrapper(parameter)) val resolver = resolvers.getResolver(ModalParameterResolver::class, request) val modalMapping = mockk { - every { asString } returns STRING - every { asStringList } returns strings - every { asMentions } returns mentions - every { asAttachmentList } returns attachments + every { getter(this@mockk) } returns value every { this@mockk.type } returns type } val value = resolver.resolveSuspend( - mockk(), + mockk { + every { isRequired } returns !(parameter.isOptional || parameter.type.isMarkedNullable) + }, mockk(), modalMapping, ) @@ -107,30 +110,38 @@ object ModalInputResolverTests { @JvmStatic fun modalInputs(): List { val listOf = listOf( - arguments("TextInput String", 0, TEXT_INPUT, STRING), - arguments("Select menu string", 16, STRING_SELECT, STRING), - arguments("Select menu strings", 1, STRING_SELECT, strings), - arguments("Select menu mentionable", 2, MENTIONABLE_SELECT, role), - arguments("Select menu mentionables", 3, MENTIONABLE_SELECT, roles), - arguments("Select menu role", 4, ROLE_SELECT, role), - arguments("Select menu roles", 5, ROLE_SELECT, roles), - arguments("Select menu user", 6, USER_SELECT, user), - arguments("Select menu users", 7, USER_SELECT, users), - arguments("Select menu input user", 8, USER_SELECT, inputUser), - arguments("Select menu input users", 9, USER_SELECT, inputUsers), - arguments("Select menu member", 10, USER_SELECT, member), - arguments("Select menu members", 11, USER_SELECT, members), - arguments("Select menu channel", 12, CHANNEL_SELECT, channel), - arguments("Select menu channels", 13, CHANNEL_SELECT, channels), - arguments("Select menu mentions", 14, MENTIONABLE_SELECT, mentions), - arguments("Attachment", 17, FILE_UPLOAD, attachments.first()), - arguments("Attachments", 15, FILE_UPLOAD, attachments), + arguments("TextInput String", 0, TEXT_INPUT, ModalMapping::getAsOptionalString, STRING, STRING), + arguments("TextInput null as null", 18, TEXT_INPUT, ModalMapping::getAsOptionalString, null, null), + arguments("TextInput null with default value", 19, TEXT_INPUT, ModalMapping::getAsOptionalString, null, null), + arguments("Select menu string", 16, STRING_SELECT, ModalMapping::getAsStringList, strings, STRING), + arguments("Select menu strings", 1, STRING_SELECT, ModalMapping::getAsStringList, strings, strings), + arguments("Select menu mentionable", 2, MENTIONABLE_SELECT, ModalMapping::getAsMentions, mentions, role), + arguments("Select menu mentionables", 3, MENTIONABLE_SELECT, ModalMapping::getAsMentions, mentions, roles), + arguments("Select menu role", 4, ROLE_SELECT, ModalMapping::getAsMentions, mentions, role), + arguments("Select menu roles", 5, ROLE_SELECT, ModalMapping::getAsMentions, mentions, roles), + arguments("Select menu user", 6, USER_SELECT, ModalMapping::getAsMentions, mentions, user), + arguments("Select menu users", 7, USER_SELECT, ModalMapping::getAsMentions, mentions, users), + arguments("Select menu input user", 8, USER_SELECT, ModalMapping::getAsMentions, mentions, inputUser), + arguments("Select menu input users", 9, USER_SELECT, ModalMapping::getAsMentions, mentions, inputUsers), + arguments("Select menu member", 10, USER_SELECT, ModalMapping::getAsMentions, mentions, member), + arguments("Select menu members", 11, USER_SELECT, ModalMapping::getAsMentions, mentions, members), + arguments("Select menu channel", 12, CHANNEL_SELECT, ModalMapping::getAsMentions, mentions, channel), + arguments("Select menu channels", 13, CHANNEL_SELECT, ModalMapping::getAsMentions, mentions, channels), + arguments("Select menu mentions", 14, MENTIONABLE_SELECT, ModalMapping::getAsMentions, mentions, mentions), + arguments("Attachment", 17, FILE_UPLOAD, ModalMapping::getAsAttachmentList, attachments, attachments.first()), + arguments("Attachments", 15, FILE_UPLOAD, ModalMapping::getAsAttachmentList, attachments, attachments), + arguments("Checkbox", 20, CHECKBOX, ModalMapping::getAsBoolean, value = true, expected = true), + arguments("Checkbox group single", 21, CHECKBOX_GROUP, ModalMapping::getAsStringList, strings, STRING), + arguments("Checkbox group list", 22, CHECKBOX_GROUP, ModalMapping::getAsStringList, strings, strings), + arguments("Checkbox group none as null", 23, CHECKBOX_GROUP, ModalMapping::getAsStringList, emptyList(), null), + arguments("Radio group single", 24, RADIO_GROUP, ModalMapping::getAsOptionalString, STRING, STRING), + arguments("Radio group none as null", 25, RADIO_GROUP, ModalMapping::getAsOptionalString, null, null), ) return listOf } - private fun arguments(name: String, index: Int, type: Component.Type, expected: Any?) = - argumentSet(name, index, type, expected) + private fun arguments(name: String, index: Int, type: Component.Type, getter: (ModalMapping) -> R, value: R, expected: Any?) = + argumentSet(name, index, type, getter, value, expected) private fun userFunc( @Suppress("unused") textInput: String, @@ -151,5 +162,13 @@ object ModalInputResolverTests { @Suppress("unused") attachments: List, @Suppress("unused") selectedString: String, @Suppress("unused") attachment: Message.Attachment, + @Suppress("unused") emptyTextInputAsNull: String?, + @Suppress("unused") emptyTextInputAsOptional: String = "default value", + @Suppress("unused") checkbox: Boolean, + @Suppress("unused") checkboxGroupSingle: String, + @Suppress("unused") checkboxGroupList: List, + @Suppress("unused") checkboxGroupNoneAsNull: String?, + @Suppress("unused") radioGroupSingle: String, + @Suppress("unused") radioGroupNoneAsNull: String?, ) {} } diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/Checkbox.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/Checkbox.kt new file mode 100644 index 000000000..9a074045b --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/Checkbox.kt @@ -0,0 +1,68 @@ +package dev.freya02.botcommands.jda.ktx.components + +import dev.freya02.botcommands.jda.ktx.components.utils.checkInit +import net.dv8tion.jda.api.components.checkbox.Checkbox + +private val DUMMY_CHECKBOX = Checkbox.of("id") + +class InlineCheckbox : InlineComponent { + + private var component: Checkbox = DUMMY_CHECKBOX + + override var uniqueId: Int + get() = component.uniqueId + set(value) { + component = component.withUniqueId(value) + } + + private var _customId: String? = null + /** The custom ID, it can be used to pass data, then be read from an interaction */ + var customId: String + get() = _customId.checkInit("custom ID") + set(value) { + component = component.withCustomId(value) + _customId = value + } + + /** + * Whether this checkbox is selected by default. + */ + var isDefault: Boolean + get() = component.isDefault + set(value) { + component = component.withDefault(value) + } + + fun build(): Checkbox { + customId.checkInit() + return component + } +} + +/** + * A component displaying a box which can be checked. Useful for simple yes/no questions. + * + * @param customId Custom identifier of this component, see [Checkbox.withCustomId] + * @param uniqueId Unique identifier of this component, see [Checkbox.withUniqueId] + * @param isDefault Whether it is checked by default + * @param block Lambda allowing further configuration + * + * @see net.dv8tion.jda.api.components.checkbox.Checkbox Checkbox + */ +inline fun Checkbox( + customId: String, + uniqueId: Int = -1, + isDefault: Boolean = false, + block: InlineCheckbox.() -> Unit = {}, +): Checkbox { + return InlineCheckbox() + .apply { + this.customId = customId + if (uniqueId != -1) + this.uniqueId = uniqueId + if (isDefault) + this.isDefault = true + block() + } + .build() +} diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/CheckboxGroup.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/CheckboxGroup.kt new file mode 100644 index 000000000..de26c70dd --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/CheckboxGroup.kt @@ -0,0 +1,110 @@ +package dev.freya02.botcommands.jda.ktx.components + +import dev.freya02.botcommands.jda.ktx.components.utils.MutableAccumulator +import dev.freya02.botcommands.jda.ktx.ranges.setRequiredRange +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroupOption + +class InlineCheckboxGroup(val builder: CheckboxGroup.Builder) : InlineComponent { + + override var uniqueId: Int + get() = builder.uniqueId + set(value) { + builder.uniqueId = value + } + + /** The custom ID, it can be used to pass data, then be read from an interaction */ + var customId: String + get() = builder.customId + set(value) { + builder.setCustomId(value) + } + + /** Options of this checkbox group, see [CheckboxGroup.Builder.addOptions] */ + val options = MutableAccumulator(builder.options) + + /** + * Whether the user must select at least [the minimum amount of options][minValues]. + * + * @see [CheckboxGroup.Builder.setRequired]. + */ + var required: Boolean + get() = builder.isRequired + set(value) { + builder.isRequired = value + } + + /** The minimum and maximum amount of values a user can select, must not exceed [CheckboxGroup.OPTIONS_MAX_AMOUNT] */ + var valueRange: IntRange + get() = builder.minValues..builder.maxValues + set(value) { + builder.setRequiredRange(value) + } + + /** The minimum amount of values a user must select, default to `1` */ + var minValues: Int + get() = builder.minValues + set(value) { + builder.setMinValues(value) + } + + /** The maximum amount of values a user can select, must not exceed [CheckboxGroup.OPTIONS_MAX_AMOUNT] */ + var maxValues: Int + get() = builder.maxValues + set(value) { + builder.setMaxValues(value) + } + + /** + * Adds an option to this checkbox group. + * + * @param label The label of this option, see [CheckboxGroupOption.withLabel] + * @param value The value of this option, this is what the bot receives, see [CheckboxGroupOption.withValue] + * @param description The description of this option, see [CheckboxGroupOption.withDescription] + * @param default Whether this option is selected by default + */ + fun option( + label: String, + value: String, + description: String? = null, + default: Boolean = false, + ) { + options += CheckboxGroupOption(label, value, description, default) + } + + fun build(): CheckboxGroup { + return builder.build() + } +} + +/** + * A component displaying a group of up to [OPTIONS_MAX_AMOUNT][CheckboxGroup.OPTIONS_MAX_AMOUNT] checkboxes + * which can be checked independently. + * + * @param customId Custom identifier of this component, see [CheckboxGroup.Builder.setCustomId] + * @param uniqueId Unique identifier of this component, see [CheckboxGroup.Builder.setUniqueId] + * @param valueRange The minimum and maximum amount of values a user can select, must not exceed [CheckboxGroup.OPTIONS_MAX_AMOUNT] + * @param required Whether the user must populate at least the minimum amount of options + * @param block Lambda allowing further configuration + * + * @see net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup + */ +inline fun CheckboxGroup( + customId: String, + uniqueId: Int = -1, + valueRange: IntRange? = null, + required: Boolean = true, + block: InlineCheckboxGroup.() -> Unit, +): CheckboxGroup { + return InlineCheckboxGroup(CheckboxGroup.create(customId)) + .apply { + if (uniqueId != -1) + this.uniqueId = uniqueId + if (valueRange != null) + this.valueRange = valueRange + if (!required) + this.required = false + block() + } + .build() +} diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/CheckboxGroups.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/CheckboxGroups.kt new file mode 100644 index 000000000..a3de1291a --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/CheckboxGroups.kt @@ -0,0 +1,34 @@ +package dev.freya02.botcommands.jda.ktx.components + +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroupOption + +/** + * Creates a [CheckboxGroupOption][net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroupOption]. + * + * @param label The label of this option, see [CheckboxGroupOption.withLabel] + * @param value The value of this option, this is what the bot receives, see [CheckboxGroupOption.withValue] + * @param description The description of this option, see [CheckboxGroupOption.withDescription] + * @param default Whether this option is selected by default + */ +fun CheckboxGroupOption( + label: String, + value: String, + description: String? = null, + default: Boolean = false, +) = CheckboxGroupOption.of(label, value, description, default) + +/** + * Adds an option to this select menu, see [CheckboxGroupOption]. + * + * @param label The label of this option, see [CheckboxGroupOption.withLabel] + * @param value The value of this option, this is what the bot receives, see [CheckboxGroupOption.withValue] + * @param description The description of this option, see [CheckboxGroupOption.withDescription] + * @param default Whether this option is selected by default + */ +fun CheckboxGroup.Builder.option( + label: String, + value: String, + description: String? = null, + default: Boolean = false, +) = addOptions(CheckboxGroupOption(label, value, description, default)) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/RadioGroup.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/RadioGroup.kt new file mode 100644 index 000000000..cbb31a7ea --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/RadioGroup.kt @@ -0,0 +1,84 @@ +package dev.freya02.botcommands.jda.ktx.components + +import dev.freya02.botcommands.jda.ktx.components.utils.MutableAccumulator +import net.dv8tion.jda.api.components.radiogroup.RadioGroup +import net.dv8tion.jda.api.components.radiogroup.RadioGroupOption + +class InlineRadioGroup(val builder: RadioGroup.Builder) : InlineComponent { + + override var uniqueId: Int + get() = builder.uniqueId + set(value) { + builder.uniqueId = value + } + + /** The custom ID, it can be used to pass data, then be read from an interaction */ + var customId: String + get() = builder.customId + set(value) { + builder.setCustomId(value) + } + + /** Options of this select menu, see [RadioGroup.Builder.addOptions] */ + val options = MutableAccumulator(builder.options) + + /** + * Whether the user must select an option. + * + * @see [RadioGroup.Builder.setRequired]. + */ + var required: Boolean + get() = builder.isRequired + set(value) { + builder.isRequired = value + } + + /** + * Adds an option to this radio group. + * + * @param label The label of this option, see [RadioGroupOption.withLabel] + * @param value The value of this option, this is what the bot receives, see [RadioGroupOption.withValue] + * @param description The description of this option, see [RadioGroupOption.withDescription] + * @param default Whether this option is selected by default + */ + fun option( + label: String, + value: String, + description: String? = null, + default: Boolean = false, + ) { + options += RadioGroupOption(label, value, description, default) + } + + fun build(): RadioGroup { + return builder.build() + } +} + +/** + * A component displaying a group of up to [OPTIONS_MAX_AMOUNT][RadioGroup.OPTIONS_MAX_AMOUNT] radio buttons, + * in which only one can be chosen. + * + * @param customId Custom identifier of this component, see [RadioGroup.Builder.setCustomId] + * @param uniqueId Unique identifier of this component, see [RadioGroup.Builder.setUniqueId] + * @param required Whether the user must select an option + * @param block Lambda allowing further configuration + * + * @see net.dv8tion.jda.api.components.radiogroup.RadioGroup RadioGroup + */ +inline fun RadioGroup( + customId: String, + uniqueId: Int = -1, + required: Boolean = true, + block: InlineRadioGroup.() -> Unit, +): RadioGroup { + return InlineRadioGroup(RadioGroup.create(customId)) + .apply { + if (uniqueId != -1) + this.uniqueId = uniqueId + if (!required) + this.required = false + block() + } + .build() +} diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/RadioGroups.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/RadioGroups.kt new file mode 100644 index 000000000..6130c979f --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/RadioGroups.kt @@ -0,0 +1,34 @@ +package dev.freya02.botcommands.jda.ktx.components + +import net.dv8tion.jda.api.components.radiogroup.RadioGroup +import net.dv8tion.jda.api.components.radiogroup.RadioGroupOption + +/** + * Creates a [RadioGroupOption][net.dv8tion.jda.api.components.radiogroup.RadioGroupOption]. + * + * @param label The label of this option, see [RadioGroupOption.withLabel] + * @param value The value of this option, this is what the bot receives, see [RadioGroupOption.withValue] + * @param description The description of this option, see [RadioGroupOption.withDescription] + * @param default Whether this option is selected by default + */ +fun RadioGroupOption( + label: String, + value: String, + description: String? = null, + default: Boolean = false, +) = RadioGroupOption.of(label, value, description, default) + +/** + * Adds an option to this select menu, see [RadioGroupOption]. + * + * @param label The label of this option, see [RadioGroupOption.withLabel] + * @param value The value of this option, this is what the bot receives, see [RadioGroupOption.withValue] + * @param description The description of this option, see [RadioGroupOption.withDescription] + * @param default Whether this option is selected by default + */ +fun RadioGroup.Builder.option( + label: String, + value: String, + description: String? = null, + default: Boolean = false, +) = addOptions(RadioGroupOption(label, value, description, default)) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/ranges/Ranges.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/ranges/Ranges.kt index 7b0ed7fa3..26076bf33 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/ranges/Ranges.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/ranges/Ranges.kt @@ -1,8 +1,16 @@ package dev.freya02.botcommands.jda.ktx.ranges +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup import net.dv8tion.jda.api.components.selections.SelectMenu /** * The minimum and maximum amount of values a user can select, must not exceed [SelectMenu.OPTIONS_MAX_AMOUNT]. */ fun > SelectMenu.Builder<*, B>.setRequiredRange(range: IntRange): B = setRequiredRange(range.first, range.last) + +/** + * Sets the minimum and maximum number of values the user has to select, must not exceed [CheckboxGroup.OPTIONS_MAX_AMOUNT]. + * + * @see CheckboxGroup.Builder.setRequiredRange + */ +fun CheckboxGroup.Builder.setRequiredRange(range: IntRange): CheckboxGroup.Builder = setRequiredRange(range.first, range.last) diff --git a/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/commands/slash/SlashModals4.kt b/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/commands/slash/SlashModals4.kt new file mode 100644 index 000000000..e7da91769 --- /dev/null +++ b/test-bot/src/test/kotlin/dev/freya02/botcommands/bot/commands/slash/SlashModals4.kt @@ -0,0 +1,74 @@ +package dev.freya02.botcommands.bot.commands.slash + +import dev.freya02.botcommands.jda.ktx.components.Checkbox +import dev.freya02.botcommands.jda.ktx.components.CheckboxGroup +import dev.freya02.botcommands.jda.ktx.components.RadioGroup +import io.github.freya022.botcommands.api.commands.annotations.Command +import io.github.freya022.botcommands.api.commands.application.slash.GuildSlashEvent +import io.github.freya022.botcommands.api.commands.application.slash.annotations.JDASlashCommand +import io.github.freya022.botcommands.api.commands.application.slash.annotations.TopLevelSlashCommandData +import io.github.freya022.botcommands.api.modals.ModalEvent +import io.github.freya022.botcommands.api.modals.Modals +import io.github.freya022.botcommands.api.modals.annotations.ModalHandler +import io.github.freya022.botcommands.api.modals.annotations.ModalInput +import io.github.freya022.botcommands.api.modals.create +import net.dv8tion.jda.api.interactions.IntegrationType +import net.dv8tion.jda.api.interactions.InteractionContextType + +private const val MODAL_NAME = "SlashModals4: modal" +private const val CHECKBOX_ID = "checkbox-id" +private const val RADIO_GROUP_ID = "radio_group-id" +private const val CHECKBOX_GROUP_ID = "checkbox_group-id" + +@Command +class SlashModals4(private val modals: Modals) { + @TopLevelSlashCommandData( + contexts = [InteractionContextType.GUILD], + integrationTypes = [IntegrationType.USER_INSTALL] + ) + @JDASlashCommand(name = "modals4") + fun onSlashModals4(event: GuildSlashEvent) { + val modal = modals.create("Modal") { + label("I like checking boxes") { + child = Checkbox(CHECKBOX_ID, isDefault = true) + } + + label("Which Discord client do you use?") { + child = RadioGroup(RADIO_GROUP_ID, required = false) { + option("Discord (Stable)", "stable", "The vanilla option", default = true) + option("Discord PTB", "ptb", "A peek into the future") + option("Discord Canary", "canary", "Living on the edge") + } + } + + label("Which modal components do you use?") { + child = CheckboxGroup(CHECKBOX_GROUP_ID) { + option("Text Inputs", "textinputs") + option("Select Menus", "selectmenus") + option("File Uploads", "fileuploads") + option("Checkbox groups", "checkboxgroups", default = true) + } + } + + bindTo(MODAL_NAME) + } + + event.replyModal(modal).queue() + } + + @ModalHandler(MODAL_NAME) + fun onModal( + event: ModalEvent, + @ModalInput(CHECKBOX_ID) doTheyLikeCheckingBoxes: Boolean, + @ModalInput(RADIO_GROUP_ID) client: String = "", + @ModalInput(CHECKBOX_GROUP_ID) features: List, + ) { + event.reply(""" + Do you like checking boxes? $doTheyLikeCheckingBoxes + On what client? $client + Using what features? $features + """.trimIndent()) + .setEphemeral(true) + .queue() + } +}