Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,11 +24,36 @@ import kotlin.reflect.KType
* Needs to be implemented alongside a [ParameterResolver] subclass.
*
* ### Types supported by default
* - [TextInput] : `String`
* - [StringSelectMenu] : `List<String>`, `String`
* - [EntitySelectMenu] : [Mentions], `T` and `List<T>` 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<String>` (can be empty)
*
* #### [EntitySelectMenu]
* - [Mentions]
* - `T` (supports `null` when none selected)
* - `List<T>` (can be empty)
*
* Where `T` is one of:
* [IMentionable], [Role], [User], [InputUser], [Member], [GuildChannel]
* - [AttachmentUpload] : `List` of [Message.Attachment], [Message.Attachment]
*
* #### [AttachmentUpload]
* - `List` of [Message.Attachment] (can be empty)
* - [Message.Attachment] (supports `null` when none selected)
*
* #### [RadioGroup]
* - `String` (supports `null` when none selected)
*
* #### [CheckboxGroup]
* - `List<String>` (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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ModalBooleanResolver, Boolean>(Boolean::class),
ModalParameterResolver<ModalBooleanResolver, Boolean> {

override suspend fun resolveSuspend(
option: ModalOption,
event: ModalEvent,
modalMapping: ModalMapping,
): Boolean = modalMapping.asBoolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -19,13 +21,20 @@ internal object ModalStringResolver :
modalMapping: ModalMapping,
): String? {
return when (modalMapping.type) {
Component.Type.STRING_SELECT -> {
Component.Type.STRING_SELECT, Component.Type.CHECKBOX_GROUP -> {
val values = modalMapping.asStringList
if (values.size > 1)
error("Cannot get a String from a string select menu with more than a single value")
if (values.size > 1) {
throwArgument(
option.kParameter.function,
"Cannot get a String from a ${modalMapping.type} with more than a single value"
)
}
values.firstOrNull()
}
Component.Type.TEXT_INPUT -> modalMapping.asString
Component.Type.TEXT_INPUT, Component.Type.RADIO_GROUP -> when {
option.isRequired -> modalMapping.asString
else -> modalMapping.asOptionalString
}
else -> error("Cannot get a String from a ${modalMapping.type} input")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,23 @@ 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<ModalMapping> {
every { asString } returns STRING
every { asOptionalString } returns null
every { asStringList } returns strings
every { asMentions } returns mentions
every { asAttachmentList } returns attachments
every { this@mockk.type } returns type
}

val value = resolver.resolveSuspend(
mockk<ModalOption>(),
mockk<ModalOption> {
every { isRequired } returns !(parameter.isOptional || parameter.type.isMarkedNullable)
},
mockk<ModalEvent>(),
modalMapping,
)
Expand All @@ -108,6 +112,8 @@ object ModalInputResolverTests {
fun modalInputs(): List<Arguments> {
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),
Expand Down Expand Up @@ -151,5 +157,7 @@ object ModalInputResolverTests {
@Suppress("unused") attachments: List<Message.Attachment>,
@Suppress("unused") selectedString: String,
@Suppress("unused") attachment: Message.Attachment,
@Suppress("unused") emptyTextInputAsNull: String?,
@Suppress("unused") emptyTextInputAsOptional: String = "default value",
) {}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -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))
Loading