Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement select menus #339

Merged
merged 6 commits into from
Jul 3, 2021
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
20 changes: 19 additions & 1 deletion common/src/main/kotlin/entity/DiscordComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dev.kord.common.entity
import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import dev.kord.common.entity.optional.OptionalInt
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand All @@ -11,6 +12,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonNames

/**
* Represent a [intractable component within a message sent in Discord](https://discord.com/developers/docs/interactions/message-components#what-are-components).
Expand All @@ -23,6 +25,10 @@ import kotlinx.serialization.encoding.Encoder
* @property url a url for link-style buttons
* @property disabled whether the button is disabled, default `false`
* @property components a list of child components (for action rows)
* @property options the select menu options
* @property placeholder the placeholder text for the select menu
* @property minValues the minimum amount of [options] allowed
* @property maxValues the maximum amount of [options] allowed
*/
@KordPreview
@Serializable
Expand All @@ -35,7 +41,13 @@ data class DiscordComponent(
val customId: Optional<String> = Optional.Missing(),
val url: Optional<String> = Optional.Missing(),
val disabled: OptionalBoolean = OptionalBoolean.Missing,
val components: Optional<List<DiscordComponent>> = Optional.Missing()
val components: Optional<List<DiscordComponent>> = Optional.Missing(),
val options: Optional<List<DiscordSelectOption>> = Optional.Missing(),
val placeholder: Optional<String> = Optional.Missing(),
@SerialName("min_values")
val minValues: OptionalInt = OptionalInt.Missing,
@SerialName("max_values")
val maxValues: OptionalInt = OptionalInt.Missing,
)

/**
Expand All @@ -62,13 +74,19 @@ sealed class ComponentType(val value: Int) {
*/
object Button : ComponentType(2)

/**
* A select menu for picking from choices.
*/
object SelectMenu : ComponentType(3)

companion object Serializer : KSerializer<ComponentType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ComponentType", PrimitiveKind.INT)

override fun deserialize(decoder: Decoder): ComponentType =
when (val value = decoder.decodeInt()) {
1 -> ActionRow
2 -> Button
3 -> SelectMenu
else -> Unknown(value)
}

Expand Down
23 changes: 23 additions & 0 deletions common/src/main/kotlin/entity/DiscordSelectOption.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.kord.common.entity

import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import kotlinx.serialization.Serializable

/**
* Represent a [select option structure](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure).
*
* @param label the user-facing name of the option, max 25 characters
* @param value the dev-define value of the option, max 100 characters
* @param description an additional description of the option, max 50 characters
* @param emoji the emoji to show in the option
* @param default whether to render this option as selected by default
*/
@Serializable
class DiscordSelectOption(
val label: String,
val value: String,
val description: Optional<String> = Optional.Missing(),
val emoji: Optional<DiscordPartialEmoji> = Optional.Missing(),
val default: OptionalBoolean = OptionalBoolean.Missing,
)
3 changes: 2 additions & 1 deletion common/src/main/kotlin/entity/Interactions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ data class InteractionCallbackData(
@SerialName("custom_id")
val customId: Optional<String> = Optional.Missing(),
@SerialName("component_type")
val componentType: Optional<ComponentType> = Optional.Missing()
val componentType: Optional<ComponentType> = Optional.Missing(),
val values: Optional<List<String>> = Optional.Missing(),
)

@Serializable(with = Option.Serializer::class)
Expand Down
22 changes: 21 additions & 1 deletion common/src/test/kotlin/json/InteractionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package json

import dev.kord.common.annotation.KordPreview
import dev.kord.common.entity.*
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.filterList
import dev.kord.common.entity.optional.orEmpty
import kotlinx.coroutines.coroutineScope
Expand Down Expand Up @@ -117,4 +118,23 @@ class InteractionTest {
}
}
}
}

@Test
fun `select menu can be deserialized`() {
val text = file("selectmenu")

val interaction = json.decodeFromString(DiscordInteraction.serializer(), text)
with(interaction) {
applicationId shouldBe "845027738276462632"
channelId shouldBe "772908445358620702"
with(data){
componentType shouldBe ComponentType.SelectMenu
customId shouldBe "class_select_1"
values shouldBe listOf("mage", "rogue")
}
guildId shouldBe "772904309264089089"
id shouldBe "847587388497854464"
}
}

}
4 changes: 4 additions & 0 deletions common/src/test/kotlin/json/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ infix fun OptionalInt?.shouldBe(value: Int?){
Assertions.assertEquals(value, this.value)
}

infix fun <T> Optional<T>.shouldBe(that: T?) {
Assertions.assertEquals(that, this.value)
}

infix fun <T> T.shouldBe(that: T) {
Assertions.assertEquals(that, this)
}
119 changes: 119 additions & 0 deletions common/src/test/resources/json/interaction/selectmenu.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{
"application_id": "845027738276462632",
"channel_id": "772908445358620702",
"data": {
"component_type":3,
"custom_id": "class_select_1",
"values": [
"mage",
"rogue"
]
},
"guild_id": "772904309264089089",
"id": "847587388497854464",
"member": {
"avatar": null,
"deaf": false,
"is_pending": false,
"joined_at": "2020-11-02T19:25:47.248000+00:00",
"mute": false,
"nick": "Bot Man",
"pending": false,
"permissions": "17179869183",
"premium_since": null,
"roles": [
"785609923542777878"
],
"user":{
"avatar": "a_d5efa99b3eeaa7dd43acca82f5692432",
"discriminator": "1337",
"id": "53908232506183680",
"public_flags": 131141,
"username": "Mason"
}
},
"message":{
"application_id": "845027738276462632",
"attachments": [],
"author": {
"avatar": null,
"bot": true,
"discriminator": "5284",
"id": "845027738276462632",
"public_flags": 0,
"username": "Interactions Test"
},
"channel_id": "772908445358620702",
"components": [
{
"components": [
{
"custom_id": "class_select_1",
"max_values": 1,
"min_values": 1,
"options": [
{
"description": "Sneak n stab",
"emoji":{
"id": "625891304148303894",
"name": "rogue"
},
"label": "Rogue",
"value": "rogue"
},
{
"description": "Turn 'em into a sheep",
"emoji":{
"id": "625891304081063986",
"name": "mage"
},
"label": "Mage",
"value": "mage"
},
{
"description": "You get heals when I'm done doing damage",
"emoji":{
"id": "625891303795982337",
"name": "priest"
},
"label": "Priest",
"value": "priest"
}
],
"placeholder": "Choose a class",
"type": 3
}
],
"type": 1
}
],
"content": "Mason is looking for new arena partners. What classes do you play?",
"edited_timestamp": null,
"embeds": [],
"flags": 0,
"id": "847587334500646933",
"interaction": {
"id": "847587333942935632",
"name": "dropdown",
"type": 2,
"user": {
"avatar": "a_d5efa99b3eeaa7dd43acca82f5692432",
"discriminator": "1337",
"id": "53908232506183680",
"public_flags": 131141,
"username": "Mason"
}
},
"mention_everyone": false,
"mention_roles":[],
"mentions":[],
"pinned": false,
"timestamp": "2021-05-27T21:29:27.956000+00:00",
"tts": false,
"type": 20,
"webhook_id": "845027738276462632"
},
"token": "UNIQUE_TOKEN",
"type": 3,
"version": 1
}
17 changes: 11 additions & 6 deletions core/src/main/kotlin/cache/data/ComponentData.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package dev.kord.core.cache.data

import dev.kord.common.entity.ButtonStyle
import dev.kord.common.entity.ComponentType
import dev.kord.common.entity.DiscordComponent
import dev.kord.common.entity.DiscordPartialEmoji
import dev.kord.common.entity.*
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import dev.kord.common.entity.optional.OptionalInt
import dev.kord.common.entity.optional.mapList
import kotlinx.serialization.Serializable

Expand All @@ -19,7 +17,11 @@ data class ComponentData(
val customId: Optional<String> = Optional.Missing(),
val url: Optional<String> = Optional.Missing(),
val disabled: OptionalBoolean = OptionalBoolean.Missing,
val components: Optional<List<ComponentData>> = Optional.Missing()
val components: Optional<List<ComponentData>> = Optional.Missing(),
val placeholder: Optional<String> = Optional.Missing(),
val minValues: OptionalInt = OptionalInt.Missing,
val maxValues: OptionalInt = OptionalInt.Missing,
val options: Optional<List<SelectOptionData>> = Optional.Missing()
) {

companion object {
Expand All @@ -33,7 +35,10 @@ data class ComponentData(
customId,
url,
disabled,
components.mapList { from(it) }
components.mapList { from(it) },
placeholder = placeholder,
minValues = minValues,
maxValues = maxValues
)
}

Expand Down
8 changes: 4 additions & 4 deletions core/src/main/kotlin/cache/data/InteractionData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ data class ApplicationInteractionData(
val options: Optional<List<OptionData>> = Optional.Missing(),
val resolvedObjectsData: Optional<ResolvedObjectsData> = Optional.Missing(),
val customId: Optional<String> = Optional.Missing(),
val componentType: Optional<ComponentType> = Optional.Missing()
val componentType: Optional<ComponentType> = Optional.Missing(),
val values: Optional<List<String>> = Optional.Missing()
) {
companion object {

Expand All @@ -109,7 +110,8 @@ data class ApplicationInteractionData(
options.map { it.map { OptionData.from(it) } },
resolved.map { ResolvedObjectsData.from(it, guildId) },
customId,
componentType
componentType,
values = values,
)
}
}
Expand Down Expand Up @@ -150,5 +152,3 @@ object NotSerializable : KSerializer<Any?> {
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun serialize(encoder: Encoder, value: Any?) = error("This operation is not supported.")
}


32 changes: 32 additions & 0 deletions core/src/main/kotlin/cache/data/SelectOptionData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dev.kord.core.cache.data

import dev.kord.common.entity.DiscordPartialEmoji
import dev.kord.common.entity.DiscordSelectOption
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import kotlinx.serialization.Serializable

@Serializable
data class SelectOptionData(
val label: String,
val value: String,
val description: Optional<String> = Optional.Missing(),
val emoji: Optional<DiscordPartialEmoji> = Optional.Missing(),
val default: OptionalBoolean = OptionalBoolean.Missing
) {

companion object {

fun from(entity: DiscordSelectOption): SelectOptionData = with(entity){
SelectOptionData(
label = label,
value = value,
description = description,
emoji = emoji,
default = default
)
}

}

}
19 changes: 15 additions & 4 deletions core/src/main/kotlin/entity/component/ActionRowComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,25 @@ class ActionRowComponent(override val data: ComponentData) : Component {
override val type: ComponentType
get() = ComponentType.ActionRow

/**
* All components nested inside this component.
*/
val components: List<Component>
get() = data.components.orEmpty().map { Component(it) }

/**
* The buttons that are nested inside this component
* The buttons that are nested inside this component.
* @see components
*/
val buttons: List<ButtonComponent>
get() = data.components.orEmpty()
.filter { it.type == ComponentType.Button }
.map { ButtonComponent(it) }
get() = components.filterIsInstance<ButtonComponent>()

/**
* The buttons that are nested inside this component.
* @see components
*/
val selectMenus: List<SelectMenuComponent>
get() = components.filterIsInstance<SelectMenuComponent>()

override fun toString(): String = "ActionRowComponent(data=$data)"

Expand Down
Loading