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

Add AutoComplete #435

Merged
merged 14 commits into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
95 changes: 71 additions & 24 deletions common/src/main/kotlin/entity/Interactions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,34 @@ import dev.kord.common.annotation.KordExperimental
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import dev.kord.common.entity.optional.OptionalSnowflake
import kotlinx.serialization.*
import kotlinx.serialization.Contextual
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.double
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull
import mu.KotlinLogging

val kordLogger = KotlinLogging.logger { }
Expand Down Expand Up @@ -64,6 +86,7 @@ class ApplicationCommandOption(
val required: OptionalBoolean = OptionalBoolean.Missing,
@OptIn(KordExperimental::class)
val choices: Optional<List<Choice<@Serializable(NotSerializable::class) Any?>>> = Optional.Missing(),
val autocomplete: OptionalBoolean = OptionalBoolean.Missing,
val options: Optional<List<ApplicationCommandOption>> = Optional.Missing(),
)

Expand Down Expand Up @@ -247,12 +270,15 @@ sealed class InteractionType(val type: Int) {
* this type exists and is needed for components even though it's not documented
*/
object Component : InteractionType(3)

object AutoComplete : InteractionType(4)
class Unknown(type: Int) : InteractionType(type)

override fun toString(): String = when (this) {
Ping -> "InteractionType.Ping($type)"
ApplicationCommand -> "InteractionType.ApplicationCommand($type)"
Component -> "InteractionType.ComponentInvoke($type)"
AutoComplete -> "InteractionType.AutoComplete($type)"
is Unknown -> "InteractionType.Unknown($type)"
}

Expand All @@ -267,6 +293,7 @@ sealed class InteractionType(val type: Int) {
1 -> Ping
2 -> ApplicationCommand
3 -> Component
4 -> AutoComplete
else -> Unknown(type)
}
}
Expand Down Expand Up @@ -306,6 +333,7 @@ sealed class Option {
element("value", JsonElement.serializer().descriptor, isOptional = true)
element("options", JsonArray.serializer().descriptor, isOptional = true)
element("type", ApplicationCommandOptionType.serializer().descriptor, isOptional = false)
element("focused", String.serializer().descriptor, isOptional = true)
}

override fun deserialize(decoder: Decoder): Option {
Expand All @@ -316,6 +344,7 @@ sealed class Option {
var jsonValue: JsonElement? = null
var jsonOptions: JsonArray? = null
var type: ApplicationCommandOptionType? = null
var focused: OptionalBoolean = OptionalBoolean.Missing
decoder.decodeStructure(descriptor) {
while (true) {
when (val index = decodeElementIndex(descriptor)) {
Expand All @@ -324,7 +353,8 @@ sealed class Option {
2 -> jsonOptions = decodeSerializableElement(descriptor, index, JsonArray.serializer())
3 -> type =
decodeSerializableElement(descriptor, index, ApplicationCommandOptionType.serializer())

4 -> focused =
decodeSerializableElement(descriptor, index, OptionalBoolean.serializer())
CompositeDecoder.DECODE_DONE -> return@decodeStructure
else -> throw SerializationException("unknown index: $index")
}
Expand Down Expand Up @@ -359,7 +389,7 @@ sealed class Option {
ApplicationCommandOptionType.Role,
ApplicationCommandOptionType.String,
ApplicationCommandOptionType.User -> CommandArgument.Serializer.deserialize(
json, jsonValue!!, name, type!!
json, jsonValue!!, name, type!!, focused
)
else -> error("unknown ApplicationCommandOptionType $type")
}
Expand Down Expand Up @@ -412,10 +442,12 @@ data class SubCommand(
sealed class CommandArgument<out T> : Option() {

abstract val value: T
abstract val focused: OptionalBoolean

class StringArgument(
override val name: String,
override val value: String
override val value: String,
override val focused: OptionalBoolean = OptionalBoolean.Missing
) : CommandArgument<String>() {
override val type: ApplicationCommandOptionType
get() = ApplicationCommandOptionType.String
Expand All @@ -425,7 +457,8 @@ sealed class CommandArgument<out T> : Option() {

class IntegerArgument(
override val name: String,
override val value: Long
override val value: Long,
override val focused: OptionalBoolean = OptionalBoolean.Missing
) : CommandArgument<Long>() {
override val type: ApplicationCommandOptionType
get() = ApplicationCommandOptionType.Integer
Expand All @@ -435,7 +468,8 @@ sealed class CommandArgument<out T> : Option() {

class NumberArgument(
override val name: String,
override val value: Double
override val value: Double,
override val focused: OptionalBoolean = OptionalBoolean.Missing
) : CommandArgument<Double>() {
override val type: ApplicationCommandOptionType
get() = ApplicationCommandOptionType.Number
Expand All @@ -445,7 +479,8 @@ sealed class CommandArgument<out T> : Option() {

class BooleanArgument(
override val name: String,
override val value: Boolean
override val value: Boolean,
override val focused: OptionalBoolean = OptionalBoolean.Missing
) : CommandArgument<Boolean>() {
override val type: ApplicationCommandOptionType
get() = ApplicationCommandOptionType.Boolean
Expand All @@ -455,7 +490,8 @@ sealed class CommandArgument<out T> : Option() {

class UserArgument(
override val name: String,
override val value: Snowflake
override val value: Snowflake,
override val focused: OptionalBoolean = OptionalBoolean.Missing
) : CommandArgument<Snowflake>() {
override val type: ApplicationCommandOptionType
get() = ApplicationCommandOptionType.User
Expand All @@ -465,7 +501,8 @@ sealed class CommandArgument<out T> : Option() {

class ChannelArgument(
override val name: String,
override val value: Snowflake
override val value: Snowflake,
override val focused: OptionalBoolean = OptionalBoolean.Missing
) : CommandArgument<Snowflake>() {
override val type: ApplicationCommandOptionType
get() = ApplicationCommandOptionType.Channel
Expand All @@ -475,7 +512,8 @@ sealed class CommandArgument<out T> : Option() {

class RoleArgument(
override val name: String,
override val value: Snowflake
override val value: Snowflake,
override val focused: OptionalBoolean = OptionalBoolean.Missing
) : CommandArgument<Snowflake>() {
override val type: ApplicationCommandOptionType
get() = ApplicationCommandOptionType.Role
Expand All @@ -485,7 +523,8 @@ sealed class CommandArgument<out T> : Option() {

class MentionableArgument(
override val name: String,
override val value: Snowflake
override val value: Snowflake,
override val focused: OptionalBoolean = OptionalBoolean.Missing
) : CommandArgument<Snowflake>() {
override val type: ApplicationCommandOptionType
get() = ApplicationCommandOptionType.Mentionable
Expand Down Expand Up @@ -542,32 +581,33 @@ sealed class CommandArgument<out T> : Option() {
json: Json,
element: JsonElement,
name: String,
type: ApplicationCommandOptionType
type: ApplicationCommandOptionType,
focused: OptionalBoolean
): CommandArgument<*> = when (type) {
ApplicationCommandOptionType.Boolean -> BooleanArgument(
name, json.decodeFromJsonElement(Boolean.serializer(), element)
name, json.decodeFromJsonElement(Boolean.serializer(), element), focused
)
ApplicationCommandOptionType.String -> StringArgument(
name, json.decodeFromJsonElement(String.serializer(), element)
name, json.decodeFromJsonElement(String.serializer(), element), focused
)
ApplicationCommandOptionType.Integer -> IntegerArgument(
name, json.decodeFromJsonElement(Long.serializer(), element)
name, json.decodeFromJsonElement(Long.serializer(), element), focused
)

ApplicationCommandOptionType.Number -> NumberArgument(
name, json.decodeFromJsonElement(Double.serializer(), element)
name, json.decodeFromJsonElement(Double.serializer(), element), focused
)
ApplicationCommandOptionType.Channel -> ChannelArgument(
name, json.decodeFromJsonElement(Snowflake.serializer(), element)
name, json.decodeFromJsonElement(Snowflake.serializer(), element), focused
)
ApplicationCommandOptionType.Mentionable -> MentionableArgument(
name, json.decodeFromJsonElement(Snowflake.serializer(), element)
name, json.decodeFromJsonElement(Snowflake.serializer(), element), focused
)
ApplicationCommandOptionType.Role -> RoleArgument(
name, json.decodeFromJsonElement(Snowflake.serializer(), element)
name, json.decodeFromJsonElement(Snowflake.serializer(), element), focused
)
ApplicationCommandOptionType.User -> UserArgument(
name, json.decodeFromJsonElement(Snowflake.serializer(), element)
name, json.decodeFromJsonElement(Snowflake.serializer(), element), focused
)
ApplicationCommandOptionType.SubCommand,
ApplicationCommandOptionType.SubCommandGroup,
Expand Down Expand Up @@ -598,7 +638,7 @@ sealed class CommandArgument<out T> : Option() {

requireNotNull(element)
requireNotNull(type)
return deserialize(json, element, name, type)
return deserialize(json, element, name, type, OptionalBoolean.Missing)
}
}
}
Expand Down Expand Up @@ -640,6 +680,7 @@ sealed class InteractionResponseType(val type: Int) {
object DeferredChannelMessageWithSource : InteractionResponseType(5)
object DeferredUpdateMessage : InteractionResponseType(6)
object UpdateMessage : InteractionResponseType(7)
object ApplicationCommandAutoCompleteResult : InteractionResponseType(8)
class Unknown(type: Int) : InteractionResponseType(type)

companion object;
Expand All @@ -656,6 +697,7 @@ sealed class InteractionResponseType(val type: Int) {
5 -> DeferredChannelMessageWithSource
6 -> DeferredUpdateMessage
7 -> UpdateMessage
8 -> ApplicationCommandAutoCompleteResult
else -> Unknown(type)
}
}
Expand Down Expand Up @@ -712,3 +754,8 @@ data class DiscordGuildApplicationCommandPermission(
}
}
}

@Serializable
data class DiscordAutoComplete<T>(
val choices: List<Choice<T>>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package dev.kord.core.behavior.interaction

import dev.kord.common.entity.Choice
import dev.kord.common.entity.DiscordAutoComplete
import dev.kord.rest.builder.interaction.IntChoiceBuilder
import dev.kord.rest.builder.interaction.NumberChoiceBuilder
import dev.kord.rest.builder.interaction.StringChoiceBuilder
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

/**
* Behavior of an AutoComplete interaction.
*
* @see respondNumber
* @see respondString
* @see respondInt
* @see respond
*/
public interface AutoCompleteInteractionBehavior : InteractionBehavior

/**
* Responds with the int choices specified by [builder].
*
* @see IntChoiceBuilder
*/
@OptIn(ExperimentalContracts::class)
public suspend inline fun AutoCompleteInteractionBehavior.respondInt(builder: IntChoiceBuilder.() -> Unit) {
contract {
callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
}

kord.rest.interaction.createIntAutoCompleteInteractionResponse(id, token, builder)
}

/**
* Responds with the number choices specified by [builder].
*
* @see NumberChoiceBuilder
*/
@OptIn(ExperimentalContracts::class)
public suspend inline fun AutoCompleteInteractionBehavior.respondNumber(builder: NumberChoiceBuilder.() -> Unit) {
contract {
callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
}

kord.rest.interaction.createNumberAutoCompleteInteractionResponse(id, token, builder)
}

/**
* Responds with the string choices specified by [builder].
*
* @see StringChoiceBuilder
*/
@OptIn(ExperimentalContracts::class)
public suspend inline fun AutoCompleteInteractionBehavior.respondString(builder: StringChoiceBuilder.() -> Unit) {
contract {
callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
}

kord.rest.interaction.createStringAutoCompleteInteractionResponse(id, token, builder)
}

/**
* Responds with [choices] to this auto-complete request.
*/
public suspend inline fun <reified T> AutoCompleteInteractionBehavior.respond(choices: List<Choice<T>>) {
kord.rest.interaction.createAutoCompleteInteractionResponse(
id,
token,
DiscordAutoComplete(choices)
)
}
20 changes: 19 additions & 1 deletion core/src/main/kotlin/entity/interaction/ContextInteraction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import dev.kord.core.Kord
import dev.kord.core.behavior.MessageBehavior
import dev.kord.core.behavior.UserBehavior
import dev.kord.core.behavior.interaction.ApplicationCommandInteractionBehavior
import dev.kord.core.behavior.interaction.AutoCompleteInteractionBehavior
import dev.kord.core.cache.data.InteractionData
import dev.kord.core.entity.Message
import dev.kord.core.entity.User
import dev.kord.core.supplier.EntitySupplier
import dev.kord.core.supplier.EntitySupplyStrategy
import java.util.*
import java.util.Objects

/**
* Represents an interaction of type [ApplicationCommand][dev.kord.common.entity.InteractionType.ApplicationCommand]
Expand Down Expand Up @@ -205,3 +206,20 @@ public class UnknownApplicationCommandInteraction(
return UnknownApplicationCommandInteraction(data, kord, strategy.supply(kord))
}
}

/**
* Interaction indicating an auto-complete request from Discord.
*
* **Follow-ups and normals responses don't work on this type**
*
* Check [AutoCompleteInteractionBehavior] for response options
*/
public class AutoCompleteInteraction(
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
override val data: InteractionData,
override val user: UserBehavior,
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
override val kord: Kord,
override val supplier: EntitySupplier = kord.defaultSupplier
) : AutoCompleteInteractionBehavior, ChatInputCommandInteraction {
override fun withStrategy(strategy: EntitySupplyStrategy<*>): Interaction =
AutoCompleteInteraction(data, user, kord, strategy.supply(kord))
}
4 changes: 4 additions & 0 deletions core/src/main/kotlin/entity/interaction/Interaction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public sealed interface Interaction : InteractionBehavior {
): Interaction {
return when {
data.type == InteractionType.Component -> ComponentInteraction(data, kord, strategy.supply(kord))
data.type == InteractionType.AutoComplete -> {
val user = User(data.user.value!!, kord, strategy.supply(kord))
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
AutoCompleteInteraction(data, user, kord, strategy.supply(kord))
}
data.guildId !is OptionalSnowflake.Missing -> GuildApplicationCommandInteraction(
data,
kord,
Expand Down
Loading