Skip to content
Merged
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 (supports `null` when none 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
Expand Up @@ -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"
}
}
}
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,17 @@ internal object ModalStringResolver :
modalMapping: ModalMapping,
): String? {
return when (modalMapping.type) {
Component.Type.STRING_SELECT -> {
Component.Type.STRING_SELECT, Component.Type.CHECKBOX_GROUP -> {
val values = modalMapping.asStringList
if (values.size > 1)
error("Cannot get a String from a string select menu with more than a single value")
if (values.size > 1) {
throwArgument(
option.kParameter.function,
"Cannot get a String from a ${modalMapping.type} with more than a single value"
)
}
values.firstOrNull()
}
Component.Type.TEXT_INPUT -> modalMapping.asString
Component.Type.TEXT_INPUT, Component.Type.RADIO_GROUP -> modalMapping.asOptionalString
else -> error("Cannot get a String from a ${modalMapping.type} input")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,43 +60,46 @@ object ModalInputResolverTests {

@MethodSource("modalInputs")
@ParameterizedTest
suspend fun `Modal input parameter can be resolved`(index: Int, type: Component.Type, expected: Any?) {
suspend fun <R> `Modal input parameter can be resolved`(index: Int, type: Component.Type, getter: (ModalMapping) -> R, value: R, expected: Any?) {
val serviceContainer = mockk<ServiceContainer> {
every { getServiceNamesForAnnotation(Resolver::class) } returns listOf(
"modalMentionsResolver",
"modalStringResolver",
"modalStringListResolver",
"modalAttachmentResolver",
"modalAttachmentListResolver",
"modalBooleanResolver",
)

every { findAnnotationOnService("modalMentionsResolver", Resolver::class) } returns Resolver(0)
every { findAnnotationOnService("modalStringResolver", Resolver::class) } returns Resolver(0)
every { findAnnotationOnService("modalStringListResolver", Resolver::class) } returns Resolver(0)
every { findAnnotationOnService("modalAttachmentResolver", Resolver::class) } returns Resolver(0)
every { findAnnotationOnService("modalAttachmentListResolver", Resolver::class) } returns Resolver(0)
every { findAnnotationOnService("modalBooleanResolver", Resolver::class) } returns Resolver(0)

every { getService("modalMentionsResolver", ParameterResolver::class) } returns ModalMentionsResolver
every { getService("modalStringResolver", ParameterResolver::class) } returns ModalStringResolver
every { getService("modalStringListResolver", ParameterResolver::class) } returns ModalStringListResolver
every { getService("modalAttachmentResolver", ParameterResolver::class) } returns ModalAttachmentResolver
every { getService("modalAttachmentListResolver", ParameterResolver::class) } returns ModalAttachmentListResolver
every { getService("modalBooleanResolver", ParameterResolver::class) } returns ModalBooleanResolver
}
val resolvers = ResolverContainer(serviceContainer, listOf(ModalIMentionableResolverFactory))

val request = ResolverRequest(ParameterWrapper(::userFunc.valueParameters[index]))
val parameter = ::userFunc.valueParameters[index]
val request = ResolverRequest(ParameterWrapper(parameter))

val resolver = resolvers.getResolver(ModalParameterResolver::class, request)
val modalMapping = mockk<ModalMapping> {
every { asString } returns STRING
every { asStringList } returns strings
every { asMentions } returns mentions
every { asAttachmentList } returns attachments
every { getter(this@mockk) } returns value
every { this@mockk.type } returns type
}

val value = resolver.resolveSuspend(
mockk<ModalOption>(),
mockk<ModalOption> {
every { isRequired } returns !(parameter.isOptional || parameter.type.isMarkedNullable)
},
mockk<ModalEvent>(),
modalMapping,
)
Expand All @@ -107,30 +110,38 @@ object ModalInputResolverTests {
@JvmStatic
fun modalInputs(): List<Arguments> {
val listOf = listOf(
arguments("TextInput String", 0, TEXT_INPUT, STRING),
arguments("Select menu string", 16, STRING_SELECT, STRING),
arguments("Select menu strings", 1, STRING_SELECT, strings),
arguments("Select menu mentionable", 2, MENTIONABLE_SELECT, role),
arguments("Select menu mentionables", 3, MENTIONABLE_SELECT, roles),
arguments("Select menu role", 4, ROLE_SELECT, role),
arguments("Select menu roles", 5, ROLE_SELECT, roles),
arguments("Select menu user", 6, USER_SELECT, user),
arguments("Select menu users", 7, USER_SELECT, users),
arguments("Select menu input user", 8, USER_SELECT, inputUser),
arguments("Select menu input users", 9, USER_SELECT, inputUsers),
arguments("Select menu member", 10, USER_SELECT, member),
arguments("Select menu members", 11, USER_SELECT, members),
arguments("Select menu channel", 12, CHANNEL_SELECT, channel),
arguments("Select menu channels", 13, CHANNEL_SELECT, channels),
arguments("Select menu mentions", 14, MENTIONABLE_SELECT, mentions),
arguments("Attachment", 17, FILE_UPLOAD, attachments.first()),
arguments("Attachments", 15, FILE_UPLOAD, attachments),
arguments("TextInput String", 0, TEXT_INPUT, ModalMapping::getAsOptionalString, STRING, STRING),
arguments("TextInput null as null", 18, TEXT_INPUT, ModalMapping::getAsOptionalString, null, null),
arguments("TextInput null with default value", 19, TEXT_INPUT, ModalMapping::getAsOptionalString, null, null),
arguments("Select menu string", 16, STRING_SELECT, ModalMapping::getAsStringList, strings, STRING),
arguments("Select menu strings", 1, STRING_SELECT, ModalMapping::getAsStringList, strings, strings),
arguments("Select menu mentionable", 2, MENTIONABLE_SELECT, ModalMapping::getAsMentions, mentions, role),
arguments("Select menu mentionables", 3, MENTIONABLE_SELECT, ModalMapping::getAsMentions, mentions, roles),
arguments("Select menu role", 4, ROLE_SELECT, ModalMapping::getAsMentions, mentions, role),
arguments("Select menu roles", 5, ROLE_SELECT, ModalMapping::getAsMentions, mentions, roles),
arguments("Select menu user", 6, USER_SELECT, ModalMapping::getAsMentions, mentions, user),
arguments("Select menu users", 7, USER_SELECT, ModalMapping::getAsMentions, mentions, users),
arguments("Select menu input user", 8, USER_SELECT, ModalMapping::getAsMentions, mentions, inputUser),
arguments("Select menu input users", 9, USER_SELECT, ModalMapping::getAsMentions, mentions, inputUsers),
arguments("Select menu member", 10, USER_SELECT, ModalMapping::getAsMentions, mentions, member),
arguments("Select menu members", 11, USER_SELECT, ModalMapping::getAsMentions, mentions, members),
arguments("Select menu channel", 12, CHANNEL_SELECT, ModalMapping::getAsMentions, mentions, channel),
arguments("Select menu channels", 13, CHANNEL_SELECT, ModalMapping::getAsMentions, mentions, channels),
arguments("Select menu mentions", 14, MENTIONABLE_SELECT, ModalMapping::getAsMentions, mentions, mentions),
arguments("Attachment", 17, FILE_UPLOAD, ModalMapping::getAsAttachmentList, attachments, attachments.first()),
arguments("Attachments", 15, FILE_UPLOAD, ModalMapping::getAsAttachmentList, attachments, attachments),
arguments("Checkbox", 20, CHECKBOX, ModalMapping::getAsBoolean, value = true, expected = true),
arguments("Checkbox group single", 21, CHECKBOX_GROUP, ModalMapping::getAsStringList, strings, STRING),
arguments("Checkbox group list", 22, CHECKBOX_GROUP, ModalMapping::getAsStringList, strings, strings),
arguments("Checkbox group none as null", 23, CHECKBOX_GROUP, ModalMapping::getAsStringList, emptyList(), null),
arguments("Radio group single", 24, RADIO_GROUP, ModalMapping::getAsOptionalString, STRING, STRING),
arguments("Radio group none as null", 25, RADIO_GROUP, ModalMapping::getAsOptionalString, null, null),
)
return listOf
}

private fun arguments(name: String, index: Int, type: Component.Type, expected: Any?) =
argumentSet(name, index, type, expected)
private fun <R> 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,
Expand All @@ -151,5 +162,13 @@ 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",
@Suppress("unused") checkbox: Boolean,
@Suppress("unused") checkboxGroupSingle: String,
@Suppress("unused") checkboxGroupList: List<String>,
@Suppress("unused") checkboxGroupNoneAsNull: String?,
@Suppress("unused") radioGroupSingle: String,
@Suppress("unused") radioGroupNoneAsNull: String?,
) {}
}
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()
}
Loading