diff --git a/BotCommands-core/src/examples/kotlin/io/github/freya022/bot/commands/slash/SlashModal.kt b/BotCommands-core/src/examples/kotlin/io/github/freya022/bot/commands/slash/SlashFormat.kt similarity index 50% rename from BotCommands-core/src/examples/kotlin/io/github/freya022/bot/commands/slash/SlashModal.kt rename to BotCommands-core/src/examples/kotlin/io/github/freya022/bot/commands/slash/SlashFormat.kt index e489b3ef8e..354ef446f0 100644 --- a/BotCommands-core/src/examples/kotlin/io/github/freya022/bot/commands/slash/SlashModal.kt +++ b/BotCommands-core/src/examples/kotlin/io/github/freya022/bot/commands/slash/SlashFormat.kt @@ -1,5 +1,7 @@ package io.github.freya022.bot.commands.slash +import dev.freya02.botcommands.jda.ktx.components.StringSelectMenu +import dev.freya02.botcommands.jda.ktx.components.TextInput import dev.freya02.botcommands.jda.ktx.messages.reply_ import io.github.freya022.botcommands.api.commands.annotations.Command import io.github.freya022.botcommands.api.commands.application.ApplicationCommand @@ -8,30 +10,43 @@ import io.github.freya022.botcommands.api.commands.application.slash.annotations import io.github.freya022.botcommands.api.modals.Modals import io.github.freya022.botcommands.api.modals.annotations.RequiresModals import io.github.freya022.botcommands.api.modals.create -import io.github.freya022.botcommands.api.modals.paragraphTextInput +import net.dv8tion.jda.api.components.textinput.TextInputStyle -private const val codeInputName = "SlashModal: codeInput" +private const val codeInputId = "SlashModal: codeInput" +private const val languageInputId = "SlashModal: languageInput" @Command @RequiresModals -class SlashModal(private val modals: Modals) : ApplicationCommand() { +class SlashFormat(private val modals: Modals) : ApplicationCommand() { + @JDASlashCommand(name = "format", description = "Formats your code") suspend fun onSlashFormat(event: GuildSlashEvent) { val modal = modals.create("Format your code") { - paragraphTextInput(codeInputName, "Code") { - minLength = 3 + label("Code") { + child = TextInput(codeInputId, TextInputStyle.PARAGRAPH) { + minLength = 3 + } + } + + label("Language") { + child = StringSelectMenu(languageInputId) { + option("Kotlin", "kt") + option("Java", "java") + } } } event.replyModal(modal).queue() val modalEvent = modal.await() - val code = modalEvent.values.first().asString + val code = modalEvent.values[0].asString + val language = modalEvent.values[1].asStringList[0] - modalEvent.reply_( - """ + modalEvent.reply_(ephemeral = true) { + content = """ Here is your formatted code: - ```kt + ```$language $code``` - """.trimIndent(), ephemeral = true).queue() + """.trimIndent() + }.queue() } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/components/AbstractComponentFactory.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/components/AbstractComponentFactory.kt index 5e5c7d71fb..75a92f7c42 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/components/AbstractComponentFactory.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/components/AbstractComponentFactory.kt @@ -10,8 +10,8 @@ import io.github.freya022.botcommands.api.components.ratelimit.ComponentRateLimi import io.github.freya022.botcommands.api.core.BContext import io.github.freya022.botcommands.internal.components.controller.ComponentController import kotlinx.coroutines.runBlocking -import net.dv8tion.jda.api.components.ActionComponent import net.dv8tion.jda.api.components.MessageTopLevelComponent +import net.dv8tion.jda.api.components.attribute.ICustomId import net.dv8tion.jda.api.components.tree.ComponentTree import javax.annotation.CheckReturnValue @@ -77,7 +77,7 @@ abstract class AbstractComponentFactory internal constructor( * and components from the same group will also be deleted according to the [timeout][ITimeoutableComponent.timeout] documentation. */ @JvmName("deleteJdaComponents") - fun deleteJdaComponentsJava(vararg components: ActionComponent) = deleteJdaComponentsJava(components.asList()) + fun deleteJdaComponentsJava(vararg components: ICustomId) = deleteJdaComponentsJava(components.asList()) /** * Removes the component data stored by the framework of the provided components. @@ -86,7 +86,7 @@ abstract class AbstractComponentFactory internal constructor( * and components from the same group will also be deleted according to the [timeout][ITimeoutableComponent.timeout] documentation. */ @JvmSynthetic - suspend fun deleteJdaComponents(vararg components: ActionComponent) = deleteJdaComponents(components.asList()) + suspend fun deleteJdaComponents(vararg components: ICustomId) = deleteJdaComponents(components.asList()) /** * Removes the component data stored by the framework of the provided components. @@ -95,7 +95,7 @@ abstract class AbstractComponentFactory internal constructor( * and components from the same group will also be deleted according to the [timeout][ITimeoutableComponent.timeout] documentation. */ @JvmName("deleteJdaComponents") - fun deleteJdaComponentsJava(components: Collection) = runBlocking { deleteJdaComponents(components) } + fun deleteJdaComponentsJava(components: Collection) = runBlocking { deleteJdaComponents(components) } /** * Removes the component data stored by the framework of the provided components. @@ -104,7 +104,7 @@ abstract class AbstractComponentFactory internal constructor( * and components from the same group will also be deleted according to the [timeout][ITimeoutableComponent.timeout] documentation. */ @JvmSynthetic - suspend fun deleteJdaComponents(components: Collection) = + suspend fun deleteJdaComponents(components: Collection) = components .mapNotNull { it.customId } .mapNotNull { IdentifiableComponent.fromIdOrNull(it) } @@ -135,7 +135,7 @@ abstract class AbstractComponentFactory internal constructor( @JvmSynthetic @JvmName("deleteRows") suspend fun deleteTreeJava(tree: ComponentTree<*>) = - tree.findAll() + tree.findAll() .mapNotNull { it.customId } .mapNotNull { IdentifiableComponent.fromIdOrNull(it) } .let { deleteComponents(it) } @@ -143,7 +143,7 @@ abstract class AbstractComponentFactory internal constructor( @JvmSynthetic suspend fun deleteTree(tree: ComponentTree<*>) = - tree.findAll() + tree.findAll() .mapNotNull { it.customId } .mapNotNull { IdentifiableComponent.fromIdOrNull(it) } .let { deleteComponents(it) } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/components/IdentifiableComponent.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/components/IdentifiableComponent.kt index 7cb1ac0b7b..707f4ae746 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/components/IdentifiableComponent.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/components/IdentifiableComponent.kt @@ -2,7 +2,7 @@ package io.github.freya022.botcommands.api.components import io.github.freya022.botcommands.internal.components.controller.ComponentController import io.github.freya022.botcommands.internal.utils.throwArgument -import net.dv8tion.jda.api.components.ActionComponent +import net.dv8tion.jda.api.components.attribute.ICustomId interface IdentifiableComponent { val internalId: Int @@ -12,16 +12,16 @@ interface IdentifiableComponent { fun isCompatible(id: String): Boolean = ComponentController.isCompatibleComponent(id) @JvmSynthetic - fun ActionComponent.toIdentifiableComponent(): IdentifiableComponent = fromComponent(this) + fun ICustomId.toIdentifiableComponent(): IdentifiableComponent = fromComponent(this) @JvmSynthetic - fun ActionComponent.toIdentifiableComponentOrNull(): IdentifiableComponent? = fromComponentOrNull(this) + fun ICustomId.toIdentifiableComponentOrNull(): IdentifiableComponent? = fromComponentOrNull(this) @JvmStatic - fun fromComponent(component: ActionComponent): IdentifiableComponent = + fun fromComponent(component: ICustomId): IdentifiableComponent = fromId(component.customId ?: throwArgument("This component has no ID")) @JvmStatic - fun fromComponentOrNull(component: ActionComponent): IdentifiableComponent? = + fun fromComponentOrNull(component: ICustomId): IdentifiableComponent? = fromIdOrNull(component.customId ?: throwArgument("This component has no ID")) @JvmStatic diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/ModalBuilder.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/ModalBuilder.kt index 6e230903eb..cc07d0078e 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/ModalBuilder.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/ModalBuilder.kt @@ -1,8 +1,18 @@ package io.github.freya022.botcommands.api.modals +import dev.freya02.botcommands.jda.ktx.components.InlineLabel +import dev.freya02.botcommands.jda.ktx.components.Label +import io.github.freya022.botcommands.api.modals.Modal as BCModal import io.github.freya022.botcommands.api.modals.annotations.ModalData import io.github.freya022.botcommands.api.modals.annotations.ModalHandler +import io.github.freya022.botcommands.api.modals.annotations.ModalInput import io.github.freya022.botcommands.internal.modals.ModalDSL +import net.dv8tion.jda.api.components.Component +import net.dv8tion.jda.api.components.ModalTopLevelComponent +import net.dv8tion.jda.api.components.label.Label +import net.dv8tion.jda.api.components.label.LabelChildComponent +import net.dv8tion.jda.api.components.tree.ComponentTree +import net.dv8tion.jda.api.modals.Modal import net.dv8tion.jda.api.modals.Modal as JDAModal import java.time.Duration as JavaDuration import java.util.concurrent.TimeUnit @@ -22,6 +32,9 @@ abstract class ModalBuilder protected constructor( /** * Binds the action to a [@ModalHandler][ModalHandler] with its arguments. * + * Each [@ModalInput][ModalInput] must match a component's custom ID, + * alternatively, you can always retrieve input values from the event. + * * @param handlerName The name of the modal handler, which must be the same as your [@ModalHandler][ModalHandler] * @param userData The optional user data to be passed to the modal handler via [@ModalData][ModalData] * @@ -33,6 +46,9 @@ abstract class ModalBuilder protected constructor( /** * Binds the action to a [@ModalHandler][ModalHandler] with its arguments. * + * Each [@ModalInput][ModalInput] must match a component's custom ID, + * alternatively, you can always retrieve input values from the event. + * * @param handlerName The name of the modal handler, which must be the same as your [@ModalHandler][ModalHandler] * @param userData The optional user data to be passed to the modal handler via [@ModalData][ModalData] * @@ -114,6 +130,20 @@ abstract class ModalBuilder protected constructor( @JvmSynthetic abstract fun timeout(timeout: Duration, onTimeout: (suspend () -> Unit)? = null): ModalBuilder + override fun setTitle(title: String): ModalBuilder = apply { super.setTitle(title) } + + override fun addComponents(components: Collection): ModalBuilder = apply { + super.addComponents(components) + } + + override fun addComponents(vararg components: ModalTopLevelComponent): ModalBuilder = apply { + super.addComponents(*components) + } + + override fun addComponents(tree: ComponentTree): ModalBuilder = apply { + super.addComponents(tree) + } + @Deprecated("Cannot set an ID on modals managed by the framework", level = DeprecationLevel.ERROR) abstract override fun setId(customId: String): ModalBuilder @@ -126,5 +156,97 @@ abstract class ModalBuilder protected constructor( } @CheckReturnValue - abstract override fun build(): Modal + abstract override fun build(): BCModal +} + +@ModalDSL +class InlineModal(val builder: ModalBuilder) { + + val components: MutableList = arrayListOf() + + operator fun ModalTopLevelComponent.unaryPlus() { + components += this + } + + operator fun Collection.unaryPlus() { + components += this + } + + /** Title of this Modal, see [Modal.Builder.setTitle] */ + var title: String + get() = builder.title + set(value) { + builder.title = value + } + + /** + * Component that contains a label, an optional description, + * and a [child component][LabelChildComponent], see [Label][net.dv8tion.jda.api.components.label.Label]. + * + * @param label Label of the Label, see [Label.withLabel] + * @param uniqueId Unique identifier of this component, see [Component.withUniqueId] + * @param description The description of this Label, see [Label.withDescription] + * @param child The child contained by this Label, see [Label.withChild] + * @param block Lambda allowing further configuration + */ + inline fun label( + label: String?, + uniqueId: Int = -1, + description: String? = null, + child: LabelChildComponent? = null, + block: InlineLabel.() -> Unit = {}, + ) { + builder.addComponents(Label(label, uniqueId, description, child, block)) + } + + /** + * Binds the action to a [@ModalHandler][ModalHandler] with its arguments. + * + * Each [@ModalInput][ModalInput] must match a component's custom ID, + * alternatively, you can always retrieve input values from the event. + * + * @param handlerName The name of the modal handler, which must be the same as your [@ModalHandler][ModalHandler] + * @param userData The optional user data to be passed to the modal handler via [@ModalData][ModalData] + */ + fun bindTo(handlerName: String, userData: List) { + builder.bindTo(handlerName, userData) + } + + /** + * Binds the action to a [@ModalHandler][ModalHandler] with its arguments. + * + * Each [@ModalInput][ModalInput] must match a component's custom ID, + * alternatively, you can always retrieve input values from the event. + * + * @param handlerName The name of the modal handler, which must be the same as your [@ModalHandler][ModalHandler] + * @param userData The optional user data to be passed to the modal handler via [@ModalData][ModalData] + */ + fun bindTo(handlerName: String, vararg userData: Any?) = builder.bindTo(handlerName, *userData) + + /** + * Binds the action to the closure. + * + * @param handler The modal handler to run when the modal is used + */ + fun bindTo(handler: suspend (ModalEvent) -> Unit) { + builder.bindTo(handler) + } + + /** + * Sets the timeout for this modal, invalidating the modal after expiration, + * and running the given timeout handler. + * + * If unset, the timeout is set to [Modals.defaultTimeout]. + * + * @param timeout The amount of time before the modal is removed + * @param onTimeout The function to run when the timeout has been reached + */ + @JvmSynthetic // Mute Java Duration test + fun timeout(timeout: Duration, onTimeout: (suspend () -> Unit)? = null) { + builder.timeout(timeout, onTimeout) + } + + fun build(): BCModal { + return builder.build() + } } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/ModalEvent.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/ModalEvent.kt index cc739c550e..48aa8eaf9b 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/ModalEvent.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/ModalEvent.kt @@ -7,7 +7,7 @@ import io.github.freya022.botcommands.internal.localization.interaction.Localiza import io.github.freya022.botcommands.internal.localization.interaction.LocalizableInteractionImpl import io.github.freya022.botcommands.internal.localization.interaction.LocalizableReplyCallbackImpl import io.github.freya022.botcommands.internal.utils.throwArgument -import net.dv8tion.jda.api.components.ActionComponent +import net.dv8tion.jda.api.components.attribute.ICustomId import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent import net.dv8tion.jda.api.interactions.modals.ModalMapping import java.util.* @@ -42,7 +42,7 @@ class ModalEvent internal constructor( override fun getRawData() = event.rawData @JvmName("getValue") - operator fun get(component: ActionComponent): ModalMapping { + operator fun get(component: ICustomId): ModalMapping { require(component.isModalCompatible) { "Can only get modal mapping for modal-compatible components, provided: $component" } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/Modals.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/Modals.kt index 3050ccd743..45ff2f8b34 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/Modals.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/Modals.kt @@ -1,13 +1,7 @@ -@file:OptIn(ExperimentalContracts::class) - package io.github.freya022.botcommands.api.modals -import dev.freya02.botcommands.jda.ktx.components.row import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService import io.github.freya022.botcommands.api.modals.Modals.Companion.defaultTimeout -import io.github.freya022.botcommands.api.modals.annotations.ModalInput -import net.dv8tion.jda.api.components.textinput.TextInput -import net.dv8tion.jda.api.components.textinput.TextInputStyle import java.time.Duration as JavaDuration import javax.annotation.CheckReturnValue import kotlin.contracts.ExperimentalContracts @@ -26,6 +20,9 @@ interface Modals { /** * Creates a new modal. * + * You can add compatible JDA components in this builder, + * see [ModalTopLevelComponent][net.dv8tion.jda.api.components.ModalTopLevelComponent]. + * * The modal expires after [a default timeout][defaultTimeout], * which can be overridden, or set by [ModalBuilder.timeout]. * @@ -34,16 +31,6 @@ interface Modals { @CheckReturnValue fun create(title: String): ModalBuilder - /** - * Creates a new text input component. - * - * @param inputName The name of the input, set in [@ModalInput][ModalInput] - * @param label The label to display on top of the text field - * @param style The style of the text field - */ - @CheckReturnValue - fun createTextInput(inputName: String, label: String, style: TextInputStyle): TextInputBuilder - companion object { @JvmSynthetic var defaultTimeout: Duration = 15.minutes @@ -58,33 +45,10 @@ interface Modals { } } -fun Modals.create(title: String, block: ModalBuilder.() -> Unit): Modal { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - return create(title).apply(block).build() -} - -fun ModalBuilder.textInput(inputName: String, label: String, inputStyle: TextInputStyle, block: TextInputBuilder.() -> Unit = {}): TextInput { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - return modals.createTextInput(inputName, label, inputStyle) - .apply(block) - .build() - .also { addComponents(row(it)) } -} - -fun ModalBuilder.shortTextInput(inputName: String, label: String, block: TextInputBuilder.() -> Unit = {}): TextInput { - contract { - callsInPlace(block, InvocationKind.EXACTLY_ONCE) - } - return textInput(inputName, label, TextInputStyle.SHORT, block) -} - -fun ModalBuilder.paragraphTextInput(inputName: String, label: String, block: TextInputBuilder.() -> Unit = {}): TextInput { +@OptIn(ExperimentalContracts::class) +inline fun Modals.create(title: String, block: InlineModal.() -> Unit): Modal { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - return textInput(inputName, label, TextInputStyle.PARAGRAPH, block) + return InlineModal(create(title)).apply(block).build() } diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/TextInputBuilder.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/TextInputBuilder.kt deleted file mode 100644 index 4974db133a..0000000000 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/TextInputBuilder.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.github.freya022.botcommands.api.modals - -import io.github.freya022.botcommands.internal.modals.ModalDSL -import net.dv8tion.jda.api.components.textinput.TextInput -import net.dv8tion.jda.api.components.textinput.TextInputStyle - -@ModalDSL -abstract class TextInputBuilder internal constructor( - label: String?, - style: TextInputStyle? -) : TextInput.Builder("0", label, style) { - @Deprecated("Cannot set an ID on text inputs managed by the framework", level = DeprecationLevel.ERROR) - override fun setId(customId: String): TextInputBuilder = this.apply { - if (customId == "0") return@apply // Super constructor call - throw UnsupportedOperationException("Cannot set an ID on text inputs managed by the framework") - } - - protected fun internetSetId(customId: String) { - super.setId(customId) - } - - override fun setLabel(label: String): TextInputBuilder = this.apply { super.setLabel(label) } - - override fun setStyle(style: TextInputStyle): TextInputBuilder = this.apply { super.setStyle(style) } - - override fun setRequired(required: Boolean): TextInputBuilder = this.apply { super.setRequired(required) } - - override fun setMinLength(minLength: Int): TextInputBuilder = this.apply { super.setMinLength(minLength) } - - override fun setMaxLength(maxLength: Int): TextInputBuilder = this.apply { super.setMaxLength(maxLength) } - - /** - * Sets the minimum and maximum required length on this TextInput component. - */ - @JvmSynthetic - fun setRequiredRange(range: IntRange) = setRequiredRange(range.first, range.last) - - override fun setRequiredRange(min: Int, max: Int): TextInputBuilder = this.apply { super.setRequiredRange(min, max) } - - override fun setValue(value: String?): TextInputBuilder = this.apply { super.setValue(value) } - - override fun setPlaceholder(placeholder: String?): TextInputBuilder = this.apply { super.setPlaceholder(placeholder) } - - protected fun jdaBuild(): TextInput { - return super.build() - } - - abstract override fun build(): TextInput -} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/annotations/ModalHandler.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/annotations/ModalHandler.kt index 2f704de5f2..4427846fbf 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/annotations/ModalHandler.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/annotations/ModalHandler.kt @@ -22,7 +22,7 @@ import io.github.freya022.botcommands.api.parameters.resolvers.ModalParameterRes * - Optionally: Have all your consecutive [@ModalData][ModalData], specified in [ModalBuilder.bindTo]. * * ### Option types - * - Input options: Uses [@ModalInput][ModalInput], the annotation's value must match the name given in [Modals.createTextInput], + * - Input options: Uses [@ModalInput][ModalInput], the annotation's value must match the Custom ID set in the input component, * supported types and modifiers are in [ParameterResolver], * additional types can be added by implementing [ModalParameterResolver]. * - [AppLocalizationContext]: Uses [@LocalizationBundle][LocalizationBundle]. diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/annotations/ModalInput.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/annotations/ModalInput.kt index f234da1e22..4f38e8c374 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/annotations/ModalInput.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/modals/annotations/ModalInput.kt @@ -1,11 +1,13 @@ package io.github.freya022.botcommands.api.modals.annotations -import io.github.freya022.botcommands.api.modals.Modals - /** * Set this parameter as a modal input. * - * The specified input name must be the same as the input name given in, for example, [Modals.createTextInput]. + * The specified input custom ID must be the same as the custom ID of a component in that modal. + * + * Supported types for each component: + * - [TextInput][net.dv8tion.jda.api.components.textinput.TextInput]: `String` + * - [StringSelectMenu][net.dv8tion.jda.api.components.selections.StringSelectMenu]: `List` * * @see ModalData @ModalData * @see ModalHandler @ModalHandler @@ -14,8 +16,7 @@ import io.github.freya022.botcommands.api.modals.Modals @Retention(AnnotationRetention.RUNTIME) annotation class ModalInput( /** - * The name of the modal input.
- * Must match the input name provided in, for example, [Modals.createTextInput]. + * The custom ID this modal input will match against. */ - @get:JvmName("value") val name: String + @get:JvmName("value") val customId: String ) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/pagination/UsedComponentSet.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/pagination/UsedComponentSet.kt index 81cb88e34f..dc78999ebd 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/pagination/UsedComponentSet.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/pagination/UsedComponentSet.kt @@ -8,8 +8,8 @@ import io.github.freya022.botcommands.api.components.IdentifiableComponent import io.github.freya022.botcommands.internal.utils.any import io.github.freya022.botcommands.internal.utils.reference import io.github.oshai.kotlinlogging.KotlinLogging -import net.dv8tion.jda.api.components.ActionComponent import net.dv8tion.jda.api.components.Component +import net.dv8tion.jda.api.components.attribute.ICustomId import net.dv8tion.jda.api.components.tree.ComponentTree import kotlin.reflect.KProperty @@ -35,7 +35,7 @@ class UsedComponentSet(private val componentsService: Components, private val cl fun setComponents(componentTree: ComponentTree<*>) { val newIds = TIntHashSet().apply { componentTree - .findAll() + .findAll() .forEach { component -> if (component.customId == null) return@forEach diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/parameters/ParameterResolver.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/parameters/ParameterResolver.kt index f6841ce544..8538c0328d 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/parameters/ParameterResolver.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/api/parameters/ParameterResolver.kt @@ -47,6 +47,7 @@ import net.dv8tion.jda.api.entities.emoji.Emoji * | [Guild] | ✓ | ✓ (as a String) | | | ✓ | | * | [Message] | | | ✓ (target message) | | | | * | [Attachment] | | ✓ | | | | | + * | [List]<[String]> | | | | | | ✓ | * * 1. The channel types are set automatically depending on the type, * but a broader channel type can be used diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/StringSelectMenuImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/StringSelectMenuImpl.kt index e633d939a6..4cc2402f4e 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/StringSelectMenuImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/components/StringSelectMenuImpl.kt @@ -4,6 +4,7 @@ import io.github.freya022.botcommands.api.components.StringSelectMenu import io.github.freya022.botcommands.api.components.event.StringSelectEvent import io.github.freya022.botcommands.internal.components.controller.ComponentController import net.dv8tion.jda.api.components.actionrow.ActionRowChildComponentUnion +import net.dv8tion.jda.api.components.label.LabelChildComponentUnion import net.dv8tion.jda.api.components.selections.StringSelectMenu as JDAStringSelectMenu internal class StringSelectMenuImpl internal constructor( @@ -13,7 +14,8 @@ internal class StringSelectMenuImpl internal constructor( ) : AbstractAwaitableComponentImpl(componentController, selectMenu), StringSelectMenu, JDAStringSelectMenu by selectMenu, - ActionRowChildComponentUnion { + ActionRowChildComponentUnion, + LabelChildComponentUnion { override fun withUniqueId(uniqueId: Int): StringSelectMenuImpl { return StringSelectMenuImpl(componentController, internalId, selectMenu.withUniqueId(uniqueId)) diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/IPartialModalData.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/IPartialModalData.kt index d072aa6581..8bbd56446f 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/IPartialModalData.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/IPartialModalData.kt @@ -1,9 +1,6 @@ package io.github.freya022.botcommands.internal.modals -import gnu.trove.map.TLongObjectMap - internal interface IPartialModalData { val handlerData: IModalHandlerData? - val inputDataMap: TLongObjectMap val timeoutInfo: ModalTimeoutInfo? -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/InputData.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/InputData.kt deleted file mode 100644 index b8c61f269b..0000000000 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/InputData.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.github.freya022.botcommands.internal.modals - -internal class InputData(val inputName: String) \ No newline at end of file diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalBuilderImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalBuilderImpl.kt index 8180fea779..b9a986de23 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalBuilderImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalBuilderImpl.kt @@ -1,17 +1,10 @@ package io.github.freya022.botcommands.internal.modals -import dev.freya02.botcommands.jda.ktx.components.findAll -import dev.freya02.botcommands.jda.ktx.components.toDefaultComponentTree -import gnu.trove.map.TLongObjectMap -import gnu.trove.map.hash.TLongObjectHashMap import io.github.freya022.botcommands.api.modals.Modal import io.github.freya022.botcommands.api.modals.ModalBuilder import io.github.freya022.botcommands.api.modals.ModalEvent import io.github.freya022.botcommands.api.modals.Modals -import io.github.freya022.botcommands.internal.utils.classRef import io.github.freya022.botcommands.internal.utils.takeIfFinite -import io.github.freya022.botcommands.internal.utils.throwState -import net.dv8tion.jda.api.components.ActionComponent import kotlin.time.Duration internal class ModalBuilderImpl internal constructor( @@ -44,22 +37,8 @@ internal class ModalBuilderImpl internal constructor( } override fun build(): Modal { - //Extract input data into this map - val inputDataMap: TLongObjectMap = TLongObjectHashMap() - components.toDefaultComponentTree() - .findAll() - .forEach { actionComponent -> - val id = actionComponent.customId ?: return@forEach - val internalId = ModalMaps.parseInputId(id) - - val data = modalMaps.consumeInput(internalId) - ?: throwState("Modal component with id '$internalId' could not be found in the inputs created with the '${classRef()}' class") - inputDataMap.put(internalId, data) - } - internetSetId(modalMaps.insertModal(PartialModalData( handlerData, - inputDataMap, timeoutInfo ?: Modals.defaultTimeout.takeIfFinite()?.let { ModalTimeoutInfo(it, null) } ))) 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 38c6bd977d..6bc37a2bd2 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 @@ -1,7 +1,5 @@ package io.github.freya022.botcommands.internal.modals -import gnu.trove.map.TObjectLongMap -import gnu.trove.map.hash.TObjectLongHashMap import io.github.freya022.botcommands.api.core.service.getService import io.github.freya022.botcommands.api.core.utils.findAnnotationRecursive import io.github.freya022.botcommands.api.core.utils.hasAnnotationRecursive @@ -64,14 +62,6 @@ internal class ModalHandlerInfo internal constructor( internal suspend fun execute(modalData: ModalData, event: ModalEvent) { val handlerData = modalData.handlerData as? PersistentModalHandlerData ?: throwInternal("This method should have not been ran as there is no handler data") - - val inputDataMap = modalData.inputDataMap - val inputNameToInputIdMap: TObjectLongMap = TObjectLongHashMap() - inputDataMap.forEachEntry { inputId: Long, inputData: InputData -> - inputNameToInputIdMap.put(inputData.inputName, inputId) - true - } - val userDatas = handlerData.userData //Check if there's enough arguments to fit user data @@ -85,7 +75,7 @@ internal class ModalHandlerInfo internal constructor( val userDataIterator = userDatas.iterator() val optionValues = parameters.mapOptions { option -> - if (tryInsertOption(event, option, inputNameToInputIdMap, userDataIterator, this) == InsertOptionResult.ABORT) + if (tryInsertOption(event, option, userDataIterator, this) == InsertOptionResult.ABORT) throwInternal(::tryInsertOption, "Insertion function shouldn't have been aborted") } @@ -95,7 +85,6 @@ internal class ModalHandlerInfo internal constructor( private suspend fun tryInsertOption( event: ModalEvent, option: OptionImpl, - inputNameToInputIdMap: TObjectLongMap, userDataIterator: Iterator, optionMap: MutableMap ): InsertOptionResult { @@ -105,10 +94,8 @@ internal class ModalHandlerInfo internal constructor( //We have the modal input's ID // But we have a Map of input *name* -> InputData (contains input ID) - val inputId = inputNameToInputIdMap[option.inputName].takeIf { it != inputNameToInputIdMap.noEntryValue } - ?: throwUser("Modal input named '${option.inputName}' was not found") - val modalMapping = event.getValue(ModalMaps.getInputId(inputId)) - ?: throwUser("Modal input ID '$inputId' was not found on the event") + val modalMapping = event.getValue(option.customId) + ?: throwUser("Modal input with custom ID '${option.customId}' was not found on the event, available values: ${event.values.map { it.customId }}") option.resolver.resolveSuspend(option, event, modalMapping).also { obj -> // Technically not required, but provides additional info diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalListener.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalListener.kt index 7b153ae572..e63fdd977f 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalListener.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalListener.kt @@ -13,6 +13,7 @@ import io.github.freya022.botcommands.internal.core.ExceptionHandler import io.github.freya022.botcommands.internal.localization.interaction.LocalizableInteractionFactory import io.github.freya022.botcommands.internal.utils.* import io.github.oshai.kotlinlogging.KotlinLogging +import net.dv8tion.jda.api.components.Component import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent import net.dv8tion.jda.api.exceptions.InsufficientPermissionException import kotlin.coroutines.resume @@ -28,12 +29,13 @@ internal class ModalListener( private val modalHandlerContainer: ModalHandlerContainer, private val modalMaps: ModalMaps, ) { + private val scope = context.coroutineScopesConfig.modalScope private val exceptionHandler = ExceptionHandler(context, logger) @BEventListener suspend fun onModalEvent(jdaEvent: ModalInteractionEvent) { - logger.trace { "Received modal interaction '${jdaEvent.modalId}' with ${jdaEvent.values.associate { it.uniqueId to it.asString }}" } + logger.trace { "Received modal interaction '${jdaEvent.modalId}' with ${jdaEvent.allValuesAsString}" } scope.launchCatching({ handleException(it, jdaEvent) }) launch@{ if (!ModalMaps.isCompatibleModal(jdaEvent.modalId)) { @@ -69,7 +71,7 @@ internal class ModalListener( private suspend fun handleException(e: Throwable, event: ModalInteractionEvent) { exceptionHandler.handleException(event, e, "modal handler, ID: '${event.modalId}'", buildMap(2) { event.message?.let { put("Message", it.jumpUrl) } - put("Modal values", event.values.associate { it.uniqueId to it.asString }) + put("Modal values", event.allValuesAsString) }) if (e is InsufficientPermissionException) { event.replyExceptionMessage(messagesFactory.get(event).missingBotPermissions(event, setOf(e.permission))) @@ -77,4 +79,13 @@ internal class ModalListener( event.replyExceptionMessage(messagesFactory.get(event).uncaughtException(event)) } } -} \ No newline at end of file + + private val ModalInteractionEvent.allValuesAsString: String + get() = values.map { value -> + when (value.type) { + Component.Type.STRING_SELECT -> value.asStringList.toString() + Component.Type.TEXT_INPUT -> value.asString + else -> value.toString() + } + }.toString() +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalMaps.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalMaps.kt index 313b1db13d..8e34790d86 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalMaps.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalMaps.kt @@ -26,9 +26,6 @@ private val logger = KotlinLogging.logger { } private const val MODAL_PREFIX = "BotCommands-Modal-" private const val MODAL_PREFIX_LENGTH = MODAL_PREFIX.length -private const val INPUT_PREFIX = "BotCommands-ModalInput-" -private const val INPUT_PREFIX_LENGTH = INPUT_PREFIX.length - private const val MAX_ID = Long.MAX_VALUE //Same amount of digits except every digit is 0 but the first one is 1 private val MIN_ID = 10.0.pow(floor(log10(MAX_ID.toDouble()))).toLong() @@ -40,13 +37,8 @@ internal class ModalMaps(context: BContext) { private val exceptionHandler = ExceptionHandler(context, logger) private val modalLock = ReentrantLock() - private val inputLock = ReentrantLock() - private val modalMap: TLongObjectMap = TLongObjectHashMap() - //Modals input IDs are temporarily stored here while it waits for its ModalBuilder owner to be built, and it's InputData to be associated with it - private val inputMap: TLongObjectMap = TLongObjectHashMap() - fun insertModal(partialModalData: PartialModalData): String { return modalLock.withLock { val internalId: Long = generateId(modalMap) @@ -76,15 +68,6 @@ internal class ModalMaps(context: BContext) { exceptionHandler.handleException(null, e, "modal timeout handler", emptyMap()) } - fun insertInput(inputData: InputData): String { - return inputLock.withLock { - val internalId: Long = generateId(inputMap) - - inputMap.put(internalId, inputData) - getInputId(internalId) - } - } - fun insertContinuation(modalId: Long, continuation: CancellableContinuation) { val data = modalMap[modalId] ?: throwInternal("Unable to find a modal with id '$modalId'") data.continuations.add(continuation) @@ -99,10 +82,6 @@ internal class ModalMaps(context: BContext) { modalMap.remove(modalId)?.also { it.cancelTimeout() } } - fun consumeInput(inputId: Long): InputData? { - inputLock.withLock { return inputMap.remove(inputId) } - } - private fun generateId(map: TLongObjectMap<*>): Long { val random = ThreadLocalRandom.current() while (true) { @@ -122,14 +101,5 @@ internal class ModalMaps(context: BContext) { return java.lang.Long.parseLong(id, MODAL_PREFIX_LENGTH, id.length, 10) } internal fun getModalId(internalId: Long): String = MODAL_PREFIX + internalId - - internal fun isCompatibleInput(id: String): Boolean = id.startsWith(INPUT_PREFIX) - internal fun parseInputId(id: String): Long { - require(isCompatibleInput(id)) { - "Cannot use JDA modal inputs ($id), please use modal inputs from ${classRef()}" - } - return java.lang.Long.parseLong(id, INPUT_PREFIX_LENGTH, id.length, 10) - } - internal fun getInputId(internalId: Long): String = INPUT_PREFIX + internalId } -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalsImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalsImpl.kt index ddd1be2440..cc3539cfad 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalsImpl.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/ModalsImpl.kt @@ -2,9 +2,7 @@ package io.github.freya022.botcommands.internal.modals import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.modals.Modals -import io.github.freya022.botcommands.api.modals.TextInputBuilder import io.github.freya022.botcommands.api.modals.annotations.RequiresModals -import net.dv8tion.jda.api.components.textinput.TextInputStyle @BService @RequiresModals @@ -12,8 +10,4 @@ internal class ModalsImpl(private val modalMaps: ModalMaps) : Modals { override fun create(title: String): ModalBuilderImpl { return ModalBuilderImpl(this, modalMaps, title) } - - override fun createTextInput(inputName: String, label: String, style: TextInputStyle): TextInputBuilder { - return TextInputBuilderImpl(modalMaps, inputName, label, style) - } -} \ No newline at end of file +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/PartialModalData.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/PartialModalData.kt index 6971a64fb9..e189c3c1b1 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/PartialModalData.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/PartialModalData.kt @@ -1,9 +1,6 @@ package io.github.freya022.botcommands.internal.modals -import gnu.trove.map.TLongObjectMap - internal open class PartialModalData( override val handlerData: IModalHandlerData?, - override val inputDataMap: TLongObjectMap, override val timeoutInfo: ModalTimeoutInfo? -) : IPartialModalData \ No newline at end of file +) : IPartialModalData diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/TextInputBuilderImpl.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/TextInputBuilderImpl.kt deleted file mode 100644 index b18fd90bff..0000000000 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/TextInputBuilderImpl.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.freya022.botcommands.internal.modals - -import io.github.freya022.botcommands.api.modals.TextInputBuilder -import net.dv8tion.jda.api.components.textinput.TextInput -import net.dv8tion.jda.api.components.textinput.TextInputStyle - -internal class TextInputBuilderImpl internal constructor( - private val modalMaps: ModalMaps, - private val inputName: String, - label: String?, - style: TextInputStyle? -) : TextInputBuilder(label, style) { - override fun build(): TextInput { - internetSetId(modalMaps.insertInput(InputData(inputName))) - - return jdaBuild() - } -} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/options/ModalHandlerInputOption.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/options/ModalHandlerInputOption.kt index f3141b4873..29b02c3848 100644 --- a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/options/ModalHandlerInputOption.kt +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/modals/options/ModalHandlerInputOption.kt @@ -13,5 +13,5 @@ internal class ModalHandlerInputOption( override val executable get() = parent.executable - val inputName: String = kParameter.findAnnotationRecursive()!!.name -} \ No newline at end of file + internal val customId: String = kParameter.findAnnotationRecursive()!!.customId +} diff --git a/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/parameters/resolvers/StringListResolver.kt b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/parameters/resolvers/StringListResolver.kt new file mode 100644 index 0000000000..94dfa41bb9 --- /dev/null +++ b/BotCommands-core/src/main/kotlin/io/github/freya022/botcommands/internal/parameters/resolvers/StringListResolver.kt @@ -0,0 +1,20 @@ +package io.github.freya022.botcommands.internal.parameters.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.TypedParameterResolver +import io.github.freya022.botcommands.api.parameters.resolvers.ModalParameterResolver +import net.dv8tion.jda.api.interactions.modals.ModalMapping +import kotlin.reflect.typeOf + +@Resolver +internal class StringListResolver : TypedParameterResolver>(typeOf>()), + ModalParameterResolver> { + + override suspend fun resolveSuspend( + option: ModalOption, + event: ModalEvent, + modalMapping: ModalMapping, + ): List = modalMapping.asStringList +} diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashInteractionMetadata.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashInteractionMetadata.kt index a6ed25d840..7f32b78048 100644 --- a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashInteractionMetadata.kt +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashInteractionMetadata.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.test.commands.slash +import dev.freya02.botcommands.jda.ktx.components.TextInput import dev.freya02.botcommands.jda.ktx.components.row import dev.freya02.botcommands.jda.ktx.messages.reply_ import io.github.freya022.botcommands.api.commands.annotations.Command @@ -13,7 +14,7 @@ import io.github.freya022.botcommands.api.commands.application.slash.annotations import io.github.freya022.botcommands.api.components.Buttons import io.github.freya022.botcommands.api.modals.Modals import io.github.freya022.botcommands.api.modals.create -import io.github.freya022.botcommands.api.modals.shortTextInput +import net.dv8tion.jda.api.components.textinput.TextInputStyle import net.dv8tion.jda.api.interactions.InteractionHook import kotlin.time.Duration.Companion.minutes @@ -41,8 +42,8 @@ class SlashInteractionMetadata( val modal = modals.create("Interaction metadata") { timeout(1.minutes) - shortTextInput("input name", "Text") { - value = "Sample text" + label("Text") { + child = TextInput("input name", TextInputStyle.SHORT, value = "Sample text") } } buttonEvent.replyModal(modal).queue() diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashLocalization.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashLocalization.kt index 409d688940..0770edffab 100644 --- a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashLocalization.kt +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashLocalization.kt @@ -1,6 +1,7 @@ package io.github.freya022.botcommands.test.commands.slash import dev.freya02.botcommands.jda.ktx.components.SelectOption +import dev.freya02.botcommands.jda.ktx.components.TextInput import dev.freya02.botcommands.jda.ktx.components.row import dev.freya02.botcommands.jda.ktx.durations.before import dev.freya02.botcommands.jda.ktx.messages.MessageCreate @@ -20,7 +21,7 @@ import io.github.freya022.botcommands.api.localization.interaction.* import io.github.freya022.botcommands.api.modals.Modals import io.github.freya022.botcommands.api.modals.annotations.RequiresModals import io.github.freya022.botcommands.api.modals.create -import io.github.freya022.botcommands.api.modals.shortTextInput +import net.dv8tion.jda.api.components.textinput.TextInputStyle import net.dv8tion.jda.api.interactions.DiscordLocale import net.dv8tion.jda.api.utils.TimeFormat import java.lang.management.ManagementFactory @@ -93,7 +94,9 @@ class SlashLocalization : ApplicationCommand() { val modalButton = buttons.primary("Open modal").ephemeral { bindTo { buttonEvent -> val modal = modals.create("Sample title") { - shortTextInput("name", "Sample label") + label("Sample label") { + child = TextInput("name", TextInputStyle.SHORT) + } } buttonEvent.replyModal(modal).queue() diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashModal.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashModal.kt index ca08a99fc5..b2697bd4ca 100644 --- a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashModal.kt +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashModal.kt @@ -1,5 +1,7 @@ package io.github.freya022.botcommands.test.commands.slash +import dev.freya02.botcommands.jda.ktx.components.StringSelectMenu +import dev.freya02.botcommands.jda.ktx.components.TextInput import dev.freya02.botcommands.jda.ktx.components.row import dev.freya02.botcommands.jda.ktx.messages.reply_ import dev.freya02.botcommands.jda.ktx.messages.send @@ -19,12 +21,14 @@ 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.annotations.RequiresModals import io.github.freya022.botcommands.api.modals.create -import io.github.freya022.botcommands.api.modals.shortTextInput import io.github.freya022.botcommands.test.CustomObject +import net.dv8tion.jda.api.components.textinput.TextInputStyle +import net.dv8tion.jda.api.interactions.IntegrationType import kotlin.time.Duration.Companion.seconds private const val SLASH_MODAL_MODAL_HANDLER = "SlashModal: modalHandler" private const val SLASH_MODAL_TEXT_INPUT = "SlashModal: textInput" +private const val SLASH_MODAL_STRING_SELECT_INPUT = "SlashModal: stringSelect" @Command @RequiresModals @@ -33,13 +37,22 @@ class SlashModal(private val buttons: Buttons) : ApplicationCommand(), GlobalApp @JDASlashCommand(name = "modal_annotated") suspend fun onSlashModal(event: GuildSlashEvent, modals: Modals) { val modal = modals.create("Title") { - shortTextInput(SLASH_MODAL_TEXT_INPUT, "Sample text") + label("Sample text") { + child = TextInput(SLASH_MODAL_TEXT_INPUT, TextInputStyle.SHORT) + } + + label("Select menu") { + child = StringSelectMenu(SLASH_MODAL_STRING_SELECT_INPUT, required = false) { + option("Opt1", "opt1") + option("Opt2", "opt2", default = true) + } + } bindTo(SLASH_MODAL_MODAL_HANDLER, "User data", 420, null) // bindTo { event -> onModalSubmitted(event, "User data", 420, event.values[0].asString, CustomObject()) } - timeout(5.seconds) { + timeout(15.seconds) { event.hook.send("Timeout !", ephemeral = true).queue() } } @@ -55,7 +68,8 @@ class SlashModal(private val buttons: Buttons) : ApplicationCommand(), GlobalApp suspend fun onModalSubmitted( event: ModalEvent, @ModalData dataStr: String, - @ModalInput(name = SLASH_MODAL_TEXT_INPUT) inputStr: String, + @ModalInput(customId = SLASH_MODAL_TEXT_INPUT) inputStr: String, + @ModalInput(customId = SLASH_MODAL_STRING_SELECT_INPUT) selectedStrings: List, @ModalData dataInt: Int, @ModalData definitelyNull: Any?, customObject: CustomObject @@ -66,6 +80,7 @@ class SlashModal(private val buttons: Buttons) : ApplicationCommand(), GlobalApp dataStr: $dataStr dataInt: $dataInt inputStr: $inputStr + selectedStrings: $selectedStrings definitelyNull: $definitelyNull customObject: $customObject """.trimIndent(), @@ -84,6 +99,7 @@ class SlashModal(private val buttons: Buttons) : ApplicationCommand(), GlobalApp override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) { manager.slashCommand("modal", function = ::onSlashModal) { + integrationTypes = IntegrationType.ALL serviceOption("modals") } } diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashServiceOption.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashServiceOption.kt index 348d82376b..0979495902 100644 --- a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashServiceOption.kt +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/slash/SlashServiceOption.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.test.commands.slash +import dev.freya02.botcommands.jda.ktx.components.TextInput import dev.freya02.botcommands.jda.ktx.components.into import dev.freya02.botcommands.jda.ktx.coroutines.await import dev.freya02.botcommands.jda.ktx.messages.reply_ @@ -23,8 +24,8 @@ 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.annotations.RequiresModals import io.github.freya022.botcommands.api.modals.create -import io.github.freya022.botcommands.api.modals.shortTextInput import net.dv8tion.jda.api.JDA +import net.dv8tion.jda.api.components.textinput.TextInputStyle import kotlin.random.Random import kotlin.time.Duration.Companion.seconds @@ -70,7 +71,9 @@ class SlashServiceOption : ApplicationCommand() { // @LocalizationBundle("MyCommands") localizationContext: AppLocalizationContext, ) { val modal = modals.create("Title") { - shortTextInput("input", "Sample text") + label("Sample text") { + child = TextInput("input", TextInputStyle.SHORT) + } bindTo("SlashServiceOption: modal", slashInput, randomNum, Random.nextDouble()) } diff --git a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/text/TextException.kt b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/text/TextException.kt index 780c743c2a..1f230dc328 100644 --- a/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/text/TextException.kt +++ b/BotCommands-core/src/test/kotlin/io/github/freya022/botcommands/test/commands/text/TextException.kt @@ -1,5 +1,6 @@ package io.github.freya022.botcommands.test.commands.text +import dev.freya02.botcommands.jda.ktx.components.TextInput import dev.freya02.botcommands.jda.ktx.components.into import dev.freya02.botcommands.jda.ktx.coroutines.await import io.github.freya022.botcommands.api.commands.annotations.Command @@ -11,7 +12,7 @@ import io.github.freya022.botcommands.api.components.annotations.RequiresCompone import io.github.freya022.botcommands.api.modals.Modals import io.github.freya022.botcommands.api.modals.annotations.RequiresModals import io.github.freya022.botcommands.api.modals.create -import io.github.freya022.botcommands.api.modals.shortTextInput +import net.dv8tion.jda.api.components.textinput.TextInputStyle @Command @RequiresModals @@ -26,7 +27,9 @@ class TextException : TextCommand() { buttons.danger("Trigger modal and exception").ephemeral() .bindTo { val modal = modals.create("Exception modal") { - shortTextInput("input name", "Sample text") + label("Sample text") { + child = TextInput("input name", TextInputStyle.SHORT) + } bindTo { throw RuntimeException("Modal exception") diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/InlineComponent.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/InlineComponent.kt index 592c3abf95..b70406a31f 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/InlineComponent.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/InlineComponent.kt @@ -6,7 +6,7 @@ import net.dv8tion.jda.api.components.Component internal annotation class InlineComponentDSL @InlineComponentDSL -internal interface InlineComponent { +interface InlineComponent { /** Unique identifier of this component, see [Component.withUniqueId] */ var uniqueId: Int } diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/Label.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/Label.kt new file mode 100644 index 0000000000..341ba374c4 --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/Label.kt @@ -0,0 +1,83 @@ +package dev.freya02.botcommands.jda.ktx.components + +import dev.freya02.botcommands.jda.ktx.components.utils.checkInit +import net.dv8tion.jda.api.components.Component +import net.dv8tion.jda.api.components.label.Label +import net.dv8tion.jda.api.components.label.LabelChildComponent +import net.dv8tion.jda.api.components.textinput.TextInput +import net.dv8tion.jda.api.components.textinput.TextInputStyle + +private val DUMMY_LABEL = Label.of("label", TextInput.create("id", TextInputStyle.SHORT).build()) + +class InlineLabel : InlineComponent { + + private var component: Label = DUMMY_LABEL + + override var uniqueId: Int + get() = component.uniqueId + set(value) { + component = component.withUniqueId(value) + } + + /** The label of this Label, see [Label.withLabel] */ + private var _label: String? = null + var label: String + get() = _label.checkInit("label content") + set(value) { + component = component.withLabel(value) + _label = value + } + + val hasLabel: Boolean get() = _label != null + + /** The description of this Label, see [Label.withDescription] */ + var description: String? + get() = component.description + set(value) { + component = component.withDescription(value) + } + + private var _child: LabelChildComponent? = null + /** The child contained by this Label, see [Label.withChild] */ + var child: LabelChildComponent + get() = _child.checkInit("child") + set(value) { + component = component.withChild(value) + _child = value + } + + val hasChild: Boolean get() = _child != null + + fun build(): Label { + child.checkInit() + return component + } +} + +/** + * Component that contains a label, an optional description, + * and a [child component][LabelChildComponent], see [Label][net.dv8tion.jda.api.components.label.Label]. + * + * @param label Label of the Label, see [Label.withLabel] + * @param uniqueId Unique identifier of this component, see [Component.withUniqueId] + * @param description The description of this Label, see [Label.withDescription] + * @param child The child contained by this Label, see [Label.withChild] + * @param block Lambda allowing further configuration + */ +inline fun Label( + label: String? = null, + uniqueId: Int = -1, + description: String? = null, + child: LabelChildComponent? = null, + block: InlineLabel.() -> Unit, +): Label { + return InlineLabel() + .apply { + if (label != null) this.label = label + if (uniqueId != -1) this.uniqueId = uniqueId + if (description != null) this.description = description + if (child != null) this.child = child + block() + } + .build() +} diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/SelectMenu.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/SelectMenu.kt new file mode 100644 index 0000000000..a14ede9ae5 --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/SelectMenu.kt @@ -0,0 +1,36 @@ +package dev.freya02.botcommands.jda.ktx.components + +import dev.freya02.botcommands.jda.ktx.ranges.setRequiredRange +import net.dv8tion.jda.api.components.selections.SelectMenu + +abstract class InlineSelectMenu : InlineComponent { + + abstract val builder: SelectMenu.Builder<*, *> + + override var uniqueId: Int + get() = builder.uniqueId + set(value) { + builder.uniqueId = value + } + + /** Whether this select menu should be disabled, defaults to `false` */ + var disabled: Boolean + get() = builder.isDisabled + set(value) { + builder.isDisabled = value + } + + /** Displayed when no selections have been made yet, see [net.dv8tion.jda.api.components.selections.SelectMenu.Builder.placeholder] */ + var placeholder: String? + get() = builder.placeholder + set(value) { + builder.placeholder = value + } + + /** The minimum and maximum amount of values a user can select, must not exceed the amount of options */ + var valueRange: IntRange + get() = builder.minValues..builder.maxValues + set(value) { + builder.setRequiredRange(value) + } +} diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/StringSelectMenu.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/StringSelectMenu.kt new file mode 100644 index 0000000000..3706940c11 --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/StringSelectMenu.kt @@ -0,0 +1,85 @@ +package dev.freya02.botcommands.jda.ktx.components + +import dev.freya02.botcommands.jda.ktx.components.utils.MutableAccumulator +import net.dv8tion.jda.api.components.selections.SelectOption +import net.dv8tion.jda.api.components.selections.StringSelectMenu +import net.dv8tion.jda.api.entities.emoji.Emoji + +class InlineStringSelectMenu(override val builder: StringSelectMenu.Builder) : InlineSelectMenu() { + + // TODO remove once JDA exposes getter + private var _required: Boolean? = null + /** + * Whether the user must populate this select menu if inside a Modal. + * + * This defaults to `true` when this is used in a Modal. + * + * See [StringSelectMenu.Builder.setRequired]. + */ + var required: Boolean? + get() = _required + set(value) { + builder.setRequired(value) + _required = value + } + + /** Options of this select menu, see [StringSelectMenu.Builder.addOptions] */ + val options = MutableAccumulator(builder.options) + + /** + * Adds an option to this select menu, see [SelectOption]. + * + * @param label The label of this option, see [SelectOption.withLabel] + * @param value The value of this option, this is what the bot receives, see [SelectOption.withValue] + * @param description The description of this option, see [SelectOption.withDescription] + * @param emoji The emoji of this option + * @param default Whether this option is selected by default + */ + fun option( + label: String, + value: String, + description: String? = null, + emoji: Emoji? = null, + default: Boolean = false, + ) { + options += SelectOption(label, value, description, emoji, default) + } + + fun build(): StringSelectMenu { + return builder.build() + } +} + +/** + * Represents a selection of options, see [StringSelectMenu][net.dv8tion.jda.api.components.selections.StringSelectMenu]. + * + * @param customId Custom identifier of this component, see [StringSelectMenu.Builder.setCustomId] + * @param uniqueId Unique identifier of this component, see [StringSelectMenu.Builder.setUniqueId] + * @param required Whether the user must populate this select menu if inside a Modal, see [StringSelectMenu.Builder.setRequired] + * @param block Lambda allowing further configuration + */ +inline fun StringSelectMenu( + customId: String, + uniqueId: Int = -1, + placeholder: String? = null, + valueRange: IntRange? = null, + required: Boolean? = null, + disabled: Boolean = false, + block: InlineStringSelectMenu.() -> Unit, +): StringSelectMenu { + return InlineStringSelectMenu(StringSelectMenu.create(customId)) + .apply { + if (uniqueId != -1) + this.uniqueId = uniqueId + if (placeholder != null) + this.placeholder = placeholder + if (valueRange != null) + this.valueRange = valueRange + if (required != null) + this.required = required + if (disabled) + this.disabled = true + block() + } + .build() +} diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/TextInput.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/TextInput.kt new file mode 100644 index 0000000000..c38370eda2 --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/TextInput.kt @@ -0,0 +1,118 @@ +package dev.freya02.botcommands.jda.ktx.components + +import net.dv8tion.jda.api.components.Component +import net.dv8tion.jda.api.components.textinput.TextInput +import net.dv8tion.jda.api.components.textinput.TextInputStyle + +class InlineTextInput( + val builder: TextInput.Builder, +) : InlineComponent { + + override var uniqueId: Int + get() = builder.uniqueId + set(value) { + builder.setUniqueId(value) + } + + /** Style for the text input, see [TextInput.Builder.setStyle] */ + var style: TextInputStyle + get() = builder.style + set(value) { + builder.setStyle(value) + } + + /** Whether the user is required to write in this TextInput, see [TextInput.Builder.setRequired] */ + var isRequired: Boolean + get() = builder.isRequired + set(value) { + builder.setRequired(value) + } + + /** Minimum and maximum required length of this TextInput, see [TextInput.Builder.setRequiredRange] */ + var range: IntRange + get() = when { + builder.minLength == -1 && builder.maxLength == -1 -> IntRange.EMPTY + builder.minLength != -1 -> builder.minLength..Int.MAX_VALUE + builder.maxLength != -1 -> 0..builder.maxLength + else -> builder.minLength..builder.maxLength + } + set(value) { + builder.setRequiredRange(value.first, value.last) + } + + /** Minimum required length of this TextInput, see [TextInput.Builder.setMinLength] */ + var minLength: Int + get() = builder.minLength + set(value) { + builder.setMinLength(value) + } + + /** Maximum required length of this TextInput, see [TextInput.Builder.setMaxLength] */ + var maxLength: Int + get() = builder.maxLength + set(value) { + builder.setMaxLength(value) + } + + /** Pre-populated text for this TextInput field, see [TextInput.Builder.setValue] */ + var value: String? + get() = builder.value + set(value) { + builder.value = value + } + + /** Short hint that describes the expected value of the input field, see [TextInput.Builder.setPlaceholder] */ + var placeholder: String? + get() = builder.placeholder + set(value) { + builder.placeholder = value + } + + fun build(): TextInput { + return builder.build() + } +} + +/** + * Discord text input, see [TextInput][net.dv8tion.jda.api.components.textinput.TextInput]. + * + * @param customId The custom ID of the input, see [TextInput.Builder.setId] + * @param style Style of text input, see [TextInput.Builder.setStyle] + * @param uniqueId Unique identifier of this component, see [Component.withUniqueId] + * @param range Minimum and maximum required length of this TextInput, see [TextInput.Builder.setRequiredRange] + * @param value Pre-populated text for this TextInput field, see [TextInput.Builder.setValue] + * @param placeholder Short hint that describes the expected value of the input field, see [TextInput.Builder.setPlaceholder] + * @param block Lambda allowing further configuration + */ +inline fun TextInput( + customId: String, + style: TextInputStyle, + uniqueId: Int = -1, + isRequired: Boolean = true, + range: IntRange? = null, + value: String? = null, + placeholder: String? = null, + block: InlineTextInput.() -> Unit = {}, +): TextInput { + return TextInput.create(customId, style) + .let(::InlineTextInput) + .apply { + if (uniqueId != -1) + this.uniqueId = uniqueId + if (!isRequired) + this.isRequired = false + if (range != null) + this.range = range + if (value != null) + this.value = value + if (placeholder != null) + this.placeholder = placeholder + block() + } + .build() +} + +/** + * Sets the minimum and maximum required length on this TextInput component. + */ +fun TextInput.Builder.setRequiredRange(range: IntRange) = setRequiredRange(range.first, range.last) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/utils/MutableAccumulator.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/utils/MutableAccumulator.kt new file mode 100644 index 0000000000..7d350403a3 --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/components/utils/MutableAccumulator.kt @@ -0,0 +1,20 @@ +package dev.freya02.botcommands.jda.ktx.components.utils + +class MutableAccumulator(val collection: MutableCollection) { + + operator fun T.unaryPlus() { + collection += this + } + + operator fun Collection.unaryPlus() { + collection += this + } + + operator fun plusAssign(collection: Collection) { + this.collection += collection + } + + operator fun plusAssign(item: T) { + this.collection += item + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 379f8ab1b4..da78388d1f 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.19.1" java-string-similarity = "2.0.0" -jda = "6.0.0-rc.3" +jda = "6.0.0-rc.4" jda-emojis = "3.0.0" jda-ktx = "0.12.0" jemoji = "1.7.4"