From f111137dc62c94609088b72740171bcfd5389f2f Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:12:25 +0100 Subject: [PATCH 01/17] Add boolean resolver for checkboxes --- .../modals/resolvers/ModalBooleanResolver.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/resolvers/ModalBooleanResolver.kt 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 +} From f4a63d78af25a4e424789a88ba9b05bbdf7f466f Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:12:43 +0100 Subject: [PATCH 02/17] Document supported types for new modal datas --- .../api/parameters/resolvers/ModalParameterResolver.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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..b0fb3dba6 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 @@ -26,6 +29,9 @@ import kotlin.reflect.KType * - [EntitySelectMenu] : [Mentions], `T` and `List` where `T` is one of: * [IMentionable], [Role], [User], [InputUser], [Member], [GuildChannel] * - [AttachmentUpload] : `List` of [Message.Attachment], [Message.Attachment] + * - [RadioGroup] : `String` + * - [CheckboxGroup] : `List` + * - [Checkbox] : `Boolean` * * @param T Type of the implementation * @param R Type of the returned resolved objects From 4bdef59c4e034945f5fb7ae529d30430b1745661 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:12:50 +0100 Subject: [PATCH 03/17] Add test command --- .../bot/commands/slash/SlashModals4.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test-bot/src/test/kotlin/dev/freya02/botcommands/bot/commands/slash/SlashModals4.kt 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..8e44631c9 --- /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 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.components.checkbox.Checkbox +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup +import net.dv8tion.jda.api.components.radiogroup.RadioGroup +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.create(CHECKBOX_ID, true) + } + + label("Which Discord client do you use?") { + child = RadioGroup.create(RADIO_GROUP_ID) + .addOption("Discord (Stable)", "stable", "The vanilla option", true) + .addOption("Discord PTB", "ptb", "A peek into the future") + .addOption("Discord Canary", "canary", "Living on the edge") + .build() + } + + label("Which modal components do you use?") { + child = CheckboxGroup.create(CHECKBOX_GROUP_ID) + .addOption("Text Inputs", "textinputs") + .addOption("Select Menus", "selectmenus") + .addOption("File Uploads", "fileuploads") + .addOption("Checkbox groups", "checkboxgroups", null, true) + .build() + } + + 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() + } +} From 5fc959599cbced6eb04a4ed7e04036d78c8dc7e1 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:19:40 +0100 Subject: [PATCH 04/17] jda-ktx: Add inline builders and extensions for new components --- .../jda/ktx/components/Checkbox.kt | 68 +++++++++++ .../jda/ktx/components/CheckboxGroup.kt | 110 ++++++++++++++++++ .../jda/ktx/components/CheckboxGroups.kt | 34 ++++++ .../jda/ktx/components/RadioGroup.kt | 84 +++++++++++++ .../jda/ktx/components/RadioGroups.kt | 34 ++++++ .../botcommands/jda/ktx/ranges/Ranges.kt | 8 ++ 6 files changed, 338 insertions(+) create mode 100644 BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/Checkbox.kt create mode 100644 BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/CheckboxGroup.kt create mode 100644 BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/CheckboxGroups.kt create mode 100644 BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/RadioGroup.kt create mode 100644 BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/RadioGroups.kt 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) From 91dedf382da890e8462d788764e4a70fc532a0d0 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:20:06 +0100 Subject: [PATCH 05/17] Use ktx in test command --- .../bot/commands/slash/SlashModals4.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) 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 index 8e44631c9..b2dcd8b5c 100644 --- 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 @@ -1,5 +1,8 @@ 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 @@ -9,9 +12,6 @@ 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.components.checkbox.Checkbox -import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup -import net.dv8tion.jda.api.components.radiogroup.RadioGroup import net.dv8tion.jda.api.interactions.IntegrationType import net.dv8tion.jda.api.interactions.InteractionContextType @@ -30,24 +30,24 @@ class SlashModals4(private val modals: Modals) { fun onSlashModals4(event: GuildSlashEvent) { val modal = modals.create("Modal") { label("I like checking boxes") { - child = Checkbox.create(CHECKBOX_ID, true) + child = Checkbox(CHECKBOX_ID, isDefault = true) } label("Which Discord client do you use?") { - child = RadioGroup.create(RADIO_GROUP_ID) - .addOption("Discord (Stable)", "stable", "The vanilla option", true) - .addOption("Discord PTB", "ptb", "A peek into the future") - .addOption("Discord Canary", "canary", "Living on the edge") - .build() + child = RadioGroup(RADIO_GROUP_ID) { + 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.create(CHECKBOX_GROUP_ID) - .addOption("Text Inputs", "textinputs") - .addOption("Select Menus", "selectmenus") - .addOption("File Uploads", "fileuploads") - .addOption("Checkbox groups", "checkboxgroups", null, true) - .build() + 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) From 4bd296ea36080af07d9b60f34570743105939e99 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:34:40 +0100 Subject: [PATCH 06/17] Use `ModalMapping#getAsOptionalString()` when option is not required --- .../internal/modals/resolvers/ModalStringResolver.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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..6b188e196 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 @@ -25,7 +25,10 @@ internal object ModalStringResolver : error("Cannot get a String from a string select menu with more than a single value") values.firstOrNull() } - Component.Type.TEXT_INPUT -> modalMapping.asString + Component.Type.TEXT_INPUT, Component.Type.RADIO_GROUP -> when { + option.isRequired -> modalMapping.asString + else -> modalMapping.asOptionalString + } else -> error("Cannot get a String from a ${modalMapping.type} input") } } From 5b9f1d9148b2052692f3ad4fa46ff75c76c0c1a4 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:38:33 +0100 Subject: [PATCH 07/17] Update `ModalParameterResolver` docs --- .../parameters/resolvers/ModalParameterResolver.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 b0fb3dba6..24b98f045 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 @@ -24,14 +24,14 @@ 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: + * - [TextInput] : `String`, when not filled by the user, empty or `null` if the parameter is explicitly nullable + * - [StringSelectMenu] : `String` (can be `null`), `List` (can be empty) + * - [EntitySelectMenu] : [Mentions], `T` (can be `null`) and `List` (can be empty) where `T` is one of: * [IMentionable], [Role], [User], [InputUser], [Member], [GuildChannel] - * - [AttachmentUpload] : `List` of [Message.Attachment], [Message.Attachment] - * - [RadioGroup] : `String` - * - [CheckboxGroup] : `List` - * - [Checkbox] : `Boolean` + * - [AttachmentUpload] : `List` of [Message.Attachment], can be empty, [Message.Attachment] + * - [RadioGroup] : `String`, `null` when none selected + * - [CheckboxGroup] : `List`, can be empty + * - [Checkbox] : (primitive)`Boolean` * * @param T Type of the implementation * @param R Type of the returned resolved objects From cd2ab0547589e33ee749d415b972b213fe33c2dc Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:38:39 +0100 Subject: [PATCH 08/17] Update test command --- .../freya02/botcommands/bot/commands/slash/SlashModals4.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index b2dcd8b5c..e7da91769 100644 --- 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 @@ -34,7 +34,7 @@ class SlashModals4(private val modals: Modals) { } label("Which Discord client do you use?") { - child = RadioGroup(RADIO_GROUP_ID) { + 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") @@ -60,7 +60,7 @@ class SlashModals4(private val modals: Modals) { fun onModal( event: ModalEvent, @ModalInput(CHECKBOX_ID) doTheyLikeCheckingBoxes: Boolean, - @ModalInput(RADIO_GROUP_ID) client: String, + @ModalInput(RADIO_GROUP_ID) client: String = "", @ModalInput(CHECKBOX_GROUP_ID) features: List, ) { event.reply(""" From d7a6e911d6464ffecd3ad6b47909153e7b9c3b4a Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:24:09 +0100 Subject: [PATCH 09/17] Update tests with `ModalMapping#getAsOptionalString` usages --- .../botcommands/modals/ModalInputResolverTests.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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..31078102b 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 @@ -84,11 +84,13 @@ object ModalInputResolverTests { } 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 { asOptionalString } returns null every { asStringList } returns strings every { asMentions } returns mentions every { asAttachmentList } returns attachments @@ -96,7 +98,9 @@ object ModalInputResolverTests { } val value = resolver.resolveSuspend( - mockk(), + mockk { + every { isRequired } returns !(parameter.isOptional || parameter.type.isMarkedNullable) + }, mockk(), modalMapping, ) @@ -108,6 +112,8 @@ object ModalInputResolverTests { fun modalInputs(): List { val listOf = listOf( arguments("TextInput String", 0, TEXT_INPUT, STRING), + arguments("TextInput empty as null", 18, TEXT_INPUT, null), + arguments("TextInput empty with default value", 19, TEXT_INPUT, null), arguments("Select menu string", 16, STRING_SELECT, STRING), arguments("Select menu strings", 1, STRING_SELECT, strings), arguments("Select menu mentionable", 2, MENTIONABLE_SELECT, role), @@ -151,5 +157,7 @@ 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", ) {} } From 5de8c0225938d6849ca17461a90234f68027beed Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:06:10 +0100 Subject: [PATCH 10/17] Fix support of checkbox group as `List` --- .../internal/modals/resolvers/ModalStringResolver.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 6b188e196..ec8f4bbd5 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,10 +21,14 @@ 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, Component.Type.RADIO_GROUP -> when { From 75d3e62e940738ff2990344ed8e633eaf57d62d2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:12:19 +0100 Subject: [PATCH 11/17] Rewrite "Types supported by default" of `ModalParameterResolver` --- .../resolvers/ModalParameterResolver.kt | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) 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 24b98f045..24247ab09 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 @@ -24,14 +24,36 @@ import kotlin.reflect.KType * Needs to be implemented alongside a [ParameterResolver] subclass. * * ### Types supported by default - * - [TextInput] : `String`, when not filled by the user, empty or `null` if the parameter is explicitly nullable - * - [StringSelectMenu] : `String` (can be `null`), `List` (can be empty) - * - [EntitySelectMenu] : [Mentions], `T` (can be `null`) and `List` (can be empty) 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], can be empty, [Message.Attachment] - * - [RadioGroup] : `String`, `null` when none selected - * - [CheckboxGroup] : `List`, can be empty - * - [Checkbox] : (primitive)`Boolean` + * + * #### [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 + * + * #### [Checkbox] + * - (primitive) `Boolean` * * @param T Type of the implementation * @param R Type of the returned resolved objects From 9869a3799a2370b822e4a5529a7af6ccd6cfe374 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:26:48 +0200 Subject: [PATCH 12/17] Always use `asOptionalString` for text inputs and radio groups So we get our error message instead of JDA's --- .../internal/modals/resolvers/ModalStringResolver.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 ec8f4bbd5..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 @@ -31,10 +31,7 @@ internal object ModalStringResolver : } values.firstOrNull() } - Component.Type.TEXT_INPUT, Component.Type.RADIO_GROUP -> when { - option.isRequired -> modalMapping.asString - else -> modalMapping.asOptionalString - } + Component.Type.TEXT_INPUT, Component.Type.RADIO_GROUP -> modalMapping.asOptionalString else -> error("Cannot get a String from a ${modalMapping.type} input") } } From 6fefe6f1e3f539a776fd1465834ffd0683c0d41f Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:27:17 +0200 Subject: [PATCH 13/17] Add missing log value mappings --- .../botcommands/internal/modals/utils/ModalMappings.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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() } From a94180c9c3a6913f538a3cc5154cc08969f1d1a8 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:29:51 +0200 Subject: [PATCH 14/17] Improve error when required value is null --- .../freya022/botcommands/internal/modals/ModalHandlerInfo.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" } } } From 39f92b46359339a8559017c2ebbf2ce6d9395985 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:47:53 +0200 Subject: [PATCH 15/17] Fix modal input tests --- .../modals/ModalInputResolverTests.kt | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) 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 31078102b..ceaa2653a 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", @@ -89,11 +89,7 @@ object ModalInputResolverTests { val resolver = resolvers.getResolver(ModalParameterResolver::class, request) val modalMapping = mockk { - every { asString } returns STRING - every { asOptionalString } returns null - every { asStringList } returns strings - every { asMentions } returns mentions - every { asAttachmentList } returns attachments + every { getter(this@mockk) } returns value every { this@mockk.type } returns type } @@ -111,32 +107,32 @@ object ModalInputResolverTests { @JvmStatic fun modalInputs(): List { val listOf = listOf( - arguments("TextInput String", 0, TEXT_INPUT, STRING), - arguments("TextInput empty as null", 18, TEXT_INPUT, null), - arguments("TextInput empty with default value", 19, TEXT_INPUT, null), - 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), ) 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, From e950702582f13ab7519d59d8874f3807514c2223 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:02:10 +0200 Subject: [PATCH 16/17] Document `null` is supported for checkbox groups as `String?` --- .../api/parameters/resolvers/ModalParameterResolver.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 24247ab09..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 @@ -50,7 +50,7 @@ import kotlin.reflect.KType * * #### [CheckboxGroup] * - `List` (can be empty) - * - `String` when a single value can be selected + * - `String` when a single value can be selected (supports `null` when none selected) * * #### [Checkbox] * - (primitive) `Boolean` From 88d94271d8e76724adeed24871186aaf911aeffa Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:04:38 +0200 Subject: [PATCH 17/17] Test new modal inputs --- .../botcommands/modals/ModalInputResolverTests.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 ceaa2653a..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 @@ -68,6 +68,7 @@ object ModalInputResolverTests { "modalStringListResolver", "modalAttachmentResolver", "modalAttachmentListResolver", + "modalBooleanResolver", ) every { findAnnotationOnService("modalMentionsResolver", Resolver::class) } returns Resolver(0) @@ -75,12 +76,14 @@ 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)) @@ -127,6 +130,12 @@ object ModalInputResolverTests { 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 } @@ -155,5 +164,11 @@ object ModalInputResolverTests { @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?, ) {} }