From 9d3f9ba15da9b07873b6403bf6bb82a8c12fadab Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:11:49 +0100 Subject: [PATCH 01/13] Update JDA --- .../src/main/kotlin/repositories-conventions.gradle.kts | 9 +++++++++ gradle/libs.versions.toml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/repositories-conventions.gradle.kts b/buildSrc/src/main/kotlin/repositories-conventions.gradle.kts index e69b970fb..f5e27fb4c 100644 --- a/buildSrc/src/main/kotlin/repositories-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/repositories-conventions.gradle.kts @@ -1,4 +1,13 @@ repositories { mavenCentral() + exclusiveContent { + forRepository { + maven("https://jitpack.io") + } + + filter { + includeModule("io.github.JDA-Fork", "JDA") + } + } mavenLocal() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81fdabadc..0322f20d3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ h2 = "2.3.232" hikaricp = "6.2.1" jackson = "2.20.0" java-string-similarity = "2.0.0" -jda = "6.3.0" +jda = "aa61a91fa0" jda-emojis = "3.0.0" jda-ktx = "0.12.0" jemoji = "1.7.5" @@ -57,7 +57,7 @@ jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", ver jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } java-string-similarity = { module = "info.debatty:java-string-similarity", version.ref = "java-string-similarity" } -jda = { module = "net.dv8tion:JDA", version.ref = "jda" } +jda = { module = "io.github.JDA-Fork:JDA", version.ref = "jda" } jda-emojis = { module = "dev.freya02:jda-emojis", version.ref = "jda-emojis" } jda-ktx = { module = "club.minnced:jda-ktx", version.ref = "jda-ktx" } jemoji = { module = "net.fellbaum:jemoji", version.ref = "jemoji" } From 665a44fc28e88f0811bcc3cbef28d4c64bf84304 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 02/13] 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 a2679bf13f86d1d262f44d4948f088df40d479b3 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 03/13] 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 f54497f2e25c78810713f1a13c984d43163bf9c7 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 04/13] 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 1dafd5f403036b67855fc24bbace71fd61987f0b 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 05/13] 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 5f443b141..04b34c67c 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,6 +1,7 @@ package dev.freya02.botcommands.jda.ktx.ranges import dev.freya02.botcommands.jda.ktx.DeprecatedInBcCore +import net.dv8tion.jda.api.components.checkboxgroup.CheckboxGroup import net.dv8tion.jda.api.components.selections.SelectMenu /** @@ -8,3 +9,10 @@ import net.dv8tion.jda.api.components.selections.SelectMenu */ @DeprecatedInBcCore 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 244f3f0932c1359f2c0b8a4dad549dd005ac2678 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 06/13] 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 19e0de3214c50d0165895d1ef5bb60ac565bbd1b Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:24:14 +0100 Subject: [PATCH 07/13] jda-ktx: Update migration snapshot --- .../src/test/resources/ksp_snapshot.zip | Bin 16321 -> 16343 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/BotCommands-jda-ktx/src/test/resources/ksp_snapshot.zip b/BotCommands-jda-ktx/src/test/resources/ksp_snapshot.zip index 4e1d3f59098bbd005ccfcb46657fb7d2a865f89b..92eeac683bb469714e7dd6ac4d4817f4e6b4e5c4 100644 GIT binary patch delta 1338 zcmX?Df4!bJz?+#xgaHI(3#=#dDlmg619dPJr3I$i4Z+lID=_uL4Ma_LWb^{leT@EK z`T=7ovtqvWW(y`)CNQIcWiO*3P$VXE(+b`hj0_C^%nS^klP8+$O6Kjyl0r6^^ z$L8nEAc{)X0Z}WICamh_PRp~u{GOjGF*LiMJqC@BVwkRNWMCILx$%Q{;)F+l5ny=RM=J@QVymI55}EHt@T6g`&ud|PSt@!jS@1Ntdy7Wh{EYqU5D48%bR_=Tg`23`?#<9fNi5d*4VWA6V zHy^pI^U=hcMV!a{$35{6JL;?VcWrO1F4}rlOV07gwm_-V?Ah^8W_`MEckp6_Jj*<} zUH{gHw@P%)vW*D|IOKnM%?&fQ$46hVtU7(#KCSNqTkU+IJT=>|A2+mc?fbGuS;)Qq zkMTGEP11kuMBkvj*G>Q(ZCB#nD24_Lt34{EwIfm08+4mhbzw$L#K^Ye79!3x&sa6t^& zsXGIrH^ksEGgyh0i2<0-H8BU%%T2)PbMg}tM=)Q*6p}zoO~b(abEZ*XTH7ohO!u0l zf~=VQ)vO#$=bKmXq9oVJU(B<>GKm(_Ohz1&KU&*N-esW-7J2{_O5_F$Sz5}2g`zE` zndAk*LR%~i6hKA=_C3oMV`X5tDb2v(0nTF(VoRg45Llsul@wTStd%s=d~vYQHY<6s z-U(_REG@uXqX*120VsN}NP!iKNsCQBX~hFpoZRNPd?HY>0z10mV=`dH>#ddKK`y%c zeD}^=potg785rD9Og%dJgLNp_uzzb@oL&MAbK+oN@Ip~DO$lt+4JCoex6OILEoZO+8ABd*l*i_>WqOtu|!h8rEx1zy$9H( rZ|tO*4(U!VuveO_ZLbOrpp(;*<$Hkvjj{kSxY^&QQXEMF!%$eEW z?>$)%pt|gEKq%kdeAR<2{YII_VV!>heFUaF6?Xmpuuq(C{?TjS%=T}d61}!{;=FFJ zk9Vw|G={vC4BoY=?|?#~Uk^QZ9KAvugWf`&(`GO;)q9I_TJnc zXL>>T><;0yCQ}aH51Jb7MFIO8K79C`d*(*k^1}<57gw{a?YZRW?82;@_IdNRg9100 zmM!9W;pnjCfTJGInasQfK0B{XEEcB)Rb%QWJ^TClV&?xGi+8edef-sbyEk9d?f+|b z^Pr(yaIs^(Ml{c(OBWyBx@hZBRVI~qg2^>(=F-KjNAjlJ=oNB|mh{?V$hpRR>wN8< zn-(gDuUL`9D-*W#gkkK=YN6sw`+EycUYmNL+4cJWb6SbHVJytMC6|0SdRAve(4yOK za~!tq|DT=0x&G=G&xcn6iu0FGIOu)&mLhBVwEAOvRQwj-y!}RC>Rv`j^fNJVFn}V? zoAjSkREPSivD3E{OT(b!R~I zW*IzY1}h0LF#yxeCgxyzuL(F^PX1-$2BVNLAS)(w zo0o&>R`Uv8lw>-Y%OXnwB;wE_Ep&j1f#C}e14AG z)hU>jfq_Mrfx#C=p*J^Jp}(a(*selLX(moVu+S+>1F+Cvpb)PRSg2ZvZ*qnO57@{_ z$~q@b0qy4CMK`ij9IW_^6~xGQR?~8~68D}HS)Sv`b`A$gy?tlY7VxG=eDv6KyD0@ZUzY?&R<7uoyiw{)qiMarSs9BkT!4k>XEsbk*L1s+S nlbjr74>2awUYg0>0BrJX14*#}Z&o&t4p|^<;AUXZHUaSf?sm`o From 3a9a9f4941be95e91b43ec7150d19be2881fe1a3 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 08/13] 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 bb4d43db0a85f1252ab32676e181655c73d317ca 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 09/13] 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 c9e2a731349341d7032ff1802063a8c915153741 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 10/13] 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 6e242ec497f03fb25a1cce489d84170a5d915854 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 11/13] 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 a041940d9f3488a6ff0ecca74ac2500adbeeec1b 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 12/13] 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 ac956c2947c982546f97ad8f10a29e689c66e1ae 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 13/13] 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