diff --git a/common/src/main/kotlin/entity/Interactions.kt b/common/src/main/kotlin/entity/Interactions.kt index 2a586aeec904..53fe1eb065d0 100644 --- a/common/src/main/kotlin/entity/Interactions.kt +++ b/common/src/main/kotlin/entity/Interactions.kt @@ -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 { } @@ -21,6 +43,9 @@ data class DiscordApplicationCommand( @SerialName("application_id") val applicationId: Snowflake, val name: String, + /** + * Don't trust the docs: This is nullable on non chat input commands. + */ val description: String?, @SerialName("guild_id") val guildId: OptionalSnowflake = OptionalSnowflake.Missing, @@ -64,6 +89,7 @@ class ApplicationCommandOption( val required: OptionalBoolean = OptionalBoolean.Missing, @OptIn(KordExperimental::class) val choices: Optional>> = Optional.Missing(), + val autocomplete: OptionalBoolean = OptionalBoolean.Missing, val options: Optional> = Optional.Missing(), ) @@ -247,12 +273,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)" } @@ -267,6 +296,7 @@ sealed class InteractionType(val type: Int) { 1 -> Ping 2 -> ApplicationCommand 3 -> Component + 4 -> AutoComplete else -> Unknown(type) } } @@ -306,6 +336,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 { @@ -316,6 +347,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)) { @@ -324,7 +356,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") } @@ -359,7 +392,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") } @@ -412,10 +445,12 @@ data class SubCommand( sealed class CommandArgument : 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() { override val type: ApplicationCommandOptionType get() = ApplicationCommandOptionType.String @@ -425,7 +460,8 @@ sealed class CommandArgument : Option() { class IntegerArgument( override val name: String, - override val value: Long + override val value: Long, + override val focused: OptionalBoolean = OptionalBoolean.Missing ) : CommandArgument() { override val type: ApplicationCommandOptionType get() = ApplicationCommandOptionType.Integer @@ -435,7 +471,8 @@ sealed class CommandArgument : Option() { class NumberArgument( override val name: String, - override val value: Double + override val value: Double, + override val focused: OptionalBoolean = OptionalBoolean.Missing ) : CommandArgument() { override val type: ApplicationCommandOptionType get() = ApplicationCommandOptionType.Number @@ -445,7 +482,8 @@ sealed class CommandArgument : Option() { class BooleanArgument( override val name: String, - override val value: Boolean + override val value: Boolean, + override val focused: OptionalBoolean = OptionalBoolean.Missing ) : CommandArgument() { override val type: ApplicationCommandOptionType get() = ApplicationCommandOptionType.Boolean @@ -455,7 +493,8 @@ sealed class CommandArgument : Option() { class UserArgument( override val name: String, - override val value: Snowflake + override val value: Snowflake, + override val focused: OptionalBoolean = OptionalBoolean.Missing ) : CommandArgument() { override val type: ApplicationCommandOptionType get() = ApplicationCommandOptionType.User @@ -465,7 +504,8 @@ sealed class CommandArgument : Option() { class ChannelArgument( override val name: String, - override val value: Snowflake + override val value: Snowflake, + override val focused: OptionalBoolean = OptionalBoolean.Missing ) : CommandArgument() { override val type: ApplicationCommandOptionType get() = ApplicationCommandOptionType.Channel @@ -475,7 +515,8 @@ sealed class CommandArgument : Option() { class RoleArgument( override val name: String, - override val value: Snowflake + override val value: Snowflake, + override val focused: OptionalBoolean = OptionalBoolean.Missing ) : CommandArgument() { override val type: ApplicationCommandOptionType get() = ApplicationCommandOptionType.Role @@ -485,7 +526,8 @@ sealed class CommandArgument : Option() { class MentionableArgument( override val name: String, - override val value: Snowflake + override val value: Snowflake, + override val focused: OptionalBoolean = OptionalBoolean.Missing ) : CommandArgument() { override val type: ApplicationCommandOptionType get() = ApplicationCommandOptionType.Mentionable @@ -542,32 +584,33 @@ sealed class CommandArgument : 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, @@ -598,7 +641,7 @@ sealed class CommandArgument : Option() { requireNotNull(element) requireNotNull(type) - return deserialize(json, element, name, type) + return deserialize(json, element, name, type, OptionalBoolean.Missing) } } } @@ -640,6 +683,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; @@ -656,6 +700,7 @@ sealed class InteractionResponseType(val type: Int) { 5 -> DeferredChannelMessageWithSource 6 -> DeferredUpdateMessage 7 -> UpdateMessage + 8 -> ApplicationCommandAutoCompleteResult else -> Unknown(type) } } @@ -712,3 +757,8 @@ data class DiscordGuildApplicationCommandPermission( } } } + +@Serializable +data class DiscordAutoComplete( + val choices: List> +) diff --git a/core/src/main/kotlin/behavior/GuildInteractionBehavior.kt b/core/src/main/kotlin/behavior/GuildInteractionBehavior.kt index e9348b853413..a9126d3cece0 100644 --- a/core/src/main/kotlin/behavior/GuildInteractionBehavior.kt +++ b/core/src/main/kotlin/behavior/GuildInteractionBehavior.kt @@ -1,27 +1,25 @@ package dev.kord.core.behavior -import dev.kord.core.behavior.interaction.InteractionBehavior import dev.kord.common.entity.Snowflake +import dev.kord.core.behavior.interaction.InteractionBehavior import dev.kord.core.entity.Guild +import dev.kord.core.entity.interaction.ActionInteraction /** - * The behavior of a [dev.kord.core.entity.interaction.Interaction][Interaction] that was invoked in a [Guild] + * The behavior of a [ActionInteraction] that was invoked in a [Guild] */ public interface GuildInteractionBehavior : InteractionBehavior { - public val guildId: Snowflake - /** * The [GuildBehavior] for the guild the command was executed in. */ public val guildBehavior: GuildBehavior get() = GuildBehavior(guildId, kord) - public suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId) public suspend fun getGuild(): Guild = supplier.getGuild(guildId) - public companion object; + public companion object } diff --git a/core/src/main/kotlin/behavior/interaction/ActionInteractionBehavior.kt b/core/src/main/kotlin/behavior/interaction/ActionInteractionBehavior.kt new file mode 100644 index 000000000000..99f705b88627 --- /dev/null +++ b/core/src/main/kotlin/behavior/interaction/ActionInteractionBehavior.kt @@ -0,0 +1,112 @@ +package dev.kord.core.behavior.interaction + +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.entity.Message +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * The behavior of a [Discord ActionInteraction](https://discord.com/developers/docs/interactions/slash-commands#interaction) which does perform an action + * (e.g. slash commands and context actions) + */ +public interface ActionInteractionBehavior : InteractionBehavior { + + /** + * Acknowledges an interaction ephemerally. + * + * @return [EphemeralInteractionResponseBehavior] Ephemeral acknowledgement of the interaction. + */ + public suspend fun acknowledgeEphemeral(): EphemeralInteractionResponseBehavior { + kord.rest.interaction.acknowledge(id, token, true) + return EphemeralInteractionResponseBehavior(applicationId, token, kord) + } + + /** + * Acknowledges an interaction. + * + * @return [PublicInteractionResponseBehavior] public acknowledgement of an interaction. + */ + public suspend fun acknowledgePublic(): PublicInteractionResponseBehavior { + kord.rest.interaction.acknowledge(id, token) + return PublicInteractionResponseBehavior(applicationId, token, kord) + } + + public suspend fun getOriginalInteractionResponse(): Message? { + return EntitySupplyStrategy.rest.supply(kord).getOriginalInteractionOrNull(applicationId, token) + } +} + + +/** + * Acknowledges an interaction and responds with [PublicInteractionResponseBehavior]. + * + * @param builder [InteractionResponseCreateBuilder] used to create a public response. + * @return [PublicInteractionResponseBehavior] public response to the interaction. + */ + +@OptIn(ExperimentalContracts::class) +public suspend inline fun ActionInteractionBehavior.respondPublic( + builder: InteractionResponseCreateBuilder.() -> Unit +): PublicInteractionResponseBehavior { + + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + + val request = InteractionResponseCreateBuilder().apply(builder).toRequest() + kord.rest.interaction.createInteractionResponse(id, token, request) + return PublicInteractionResponseBehavior(applicationId, token, kord) + +} + + +/** + * Acknowledges an interaction and responds with [EphemeralInteractionResponseBehavior] with ephemeral flag. + * + * @param builder [InteractionResponseCreateBuilder] used to a create an ephemeral response. + * @return [InteractionResponseBehavior] ephemeral response to the interaction. + */ + +@OptIn(ExperimentalContracts::class) +public suspend inline fun ActionInteractionBehavior.respondEphemeral( + builder: InteractionResponseCreateBuilder.() -> Unit +): EphemeralInteractionResponseBehavior { + + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val builder = InteractionResponseCreateBuilder(true).apply(builder) + val request = builder.toRequest() + kord.rest.interaction.createInteractionResponse(id, token, request) + return EphemeralInteractionResponseBehavior(applicationId, token, kord) + +} + +public fun InteractionBehavior( + id: Snowflake, + channelId: Snowflake, + token: String, + applicationId: Snowflake, + kord: Kord, + strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy +): ActionInteractionBehavior = object : ActionInteractionBehavior { + override val id: Snowflake + get() = id + + override val token: String + get() = token + + override val applicationId: Snowflake + get() = applicationId + + override val kord: Kord + get() = kord + + override val channelId: Snowflake + get() = channelId + + + override val supplier: EntitySupplier = strategy.supply(kord) + +} diff --git a/core/src/main/kotlin/behavior/interaction/ApplicationCommandInteractionBehavior.kt b/core/src/main/kotlin/behavior/interaction/ApplicationCommandInteractionBehavior.kt index 05f69dc87853..9cdf5bb872dc 100644 --- a/core/src/main/kotlin/behavior/interaction/ApplicationCommandInteractionBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/ApplicationCommandInteractionBehavior.kt @@ -5,11 +5,10 @@ import dev.kord.core.Kord import dev.kord.core.supplier.EntitySupplier /** - * The behavior of a [Discord Interaction](https://discord.com/developers/docs/interactions/slash-commands#interaction) + * The behavior of a [Discord ActionInteraction](https://discord.com/developers/docs/interactions/slash-commands#interaction) * with [Application Command type][dev.kord.common.entity.ApplicationCommandType] */ - -public interface ApplicationCommandInteractionBehavior : InteractionBehavior +public interface ApplicationCommandInteractionBehavior : ActionInteractionBehavior internal fun ApplicationCommandInteractionBehavior( id: Snowflake, diff --git a/core/src/main/kotlin/behavior/interaction/AutoCompleteInteractionBehavior.kt b/core/src/main/kotlin/behavior/interaction/AutoCompleteInteractionBehavior.kt new file mode 100644 index 000000000000..a799c94e0060 --- /dev/null +++ b/core/src/main/kotlin/behavior/interaction/AutoCompleteInteractionBehavior.kt @@ -0,0 +1,80 @@ +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]. + * + * The provided choices are only suggestions and the user can provide any other input as well. + * + * @see IntChoiceBuilder + */ +@OptIn(ExperimentalContracts::class) +public suspend inline fun AutoCompleteInteractionBehavior.suggestInt(builder: IntChoiceBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + kord.rest.interaction.createIntAutoCompleteInteractionResponse(id, token, builder) +} + +/** + * Responds with the number choices specified by [builder]. + * + * The provided choices are only suggestions and the user can provide any other input as well. + * @see NumberChoiceBuilder + */ +@OptIn(ExperimentalContracts::class) +public suspend inline fun AutoCompleteInteractionBehavior.suggestNumber(builder: NumberChoiceBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + kord.rest.interaction.createNumberAutoCompleteInteractionResponse(id, token, builder) +} + +/** + * Responds with the string choices specified by [builder]. + * + * The provided choices are only suggestions and the user can provide any other input as well. + * + * @see StringChoiceBuilder + */ +@OptIn(ExperimentalContracts::class) +public suspend inline fun AutoCompleteInteractionBehavior.suggestString(builder: StringChoiceBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + kord.rest.interaction.createStringAutoCompleteInteractionResponse(id, token, builder) +} + +/** + * Responds with [choices] to this auto-complete request. + * + * The provided choices are only suggestions and the user can provide any other input as well. + */ +public suspend inline fun AutoCompleteInteractionBehavior.suggest(choices: List>) { + kord.rest.interaction.createAutoCompleteInteractionResponse( + id, + token, + DiscordAutoComplete(choices) + ) +} diff --git a/core/src/main/kotlin/behavior/interaction/ComponentInteractionBehavior.kt b/core/src/main/kotlin/behavior/interaction/ComponentInteractionBehavior.kt index cf2dfd7c22a8..81f399dde332 100644 --- a/core/src/main/kotlin/behavior/interaction/ComponentInteractionBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/ComponentInteractionBehavior.kt @@ -16,7 +16,7 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract -public interface ComponentInteractionBehavior : InteractionBehavior { +public interface ComponentInteractionBehavior : ActionInteractionBehavior { /** * Acknowledges a component interaction publicly with the intent of updating it later. diff --git a/core/src/main/kotlin/behavior/interaction/EphemeralInteractionResponseBehavior.kt b/core/src/main/kotlin/behavior/interaction/EphemeralInteractionResponseBehavior.kt index 581a7c016223..f28e81ac1072 100644 --- a/core/src/main/kotlin/behavior/interaction/EphemeralInteractionResponseBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/EphemeralInteractionResponseBehavior.kt @@ -1,11 +1,10 @@ package dev.kord.core.behavior.interaction -import dev.kord.common.annotation.KordUnsafe import dev.kord.common.entity.Snowflake import dev.kord.core.Kord /** - * The behavior of a ephemeral [Discord Interaction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) + * The behavior of a ephemeral [Discord ActionInteraction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) * This response is visible to *only* to the user who made the interaction. */ diff --git a/core/src/main/kotlin/behavior/interaction/InteractionBehavior.kt b/core/src/main/kotlin/behavior/interaction/InteractionBehavior.kt index b99eb06592ab..fe573022bb6d 100644 --- a/core/src/main/kotlin/behavior/interaction/InteractionBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/InteractionBehavior.kt @@ -1,27 +1,15 @@ package dev.kord.core.behavior.interaction import dev.kord.common.entity.Snowflake -import dev.kord.core.Kord import dev.kord.core.behavior.channel.MessageChannelBehavior import dev.kord.core.entity.KordEntity -import dev.kord.core.entity.Message import dev.kord.core.entity.Strategizable import dev.kord.core.entity.channel.MessageChannel -import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull -import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -/** - * The behavior of a [Discord Interaction](https://discord.com/developers/docs/interactions/slash-commands#interaction) - */ public interface InteractionBehavior : KordEntity, Strategizable { - public val applicationId: Snowflake public val token: String public val channelId: Snowflake @@ -33,107 +21,8 @@ public interface InteractionBehavior : KordEntity, Strategizable { public suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId) - public suspend fun getChannel(): MessageChannel = supplier.getChannelOf(channelId) - - /** - * Acknowledges an interaction ephemerally. - * - * @return [EphemeralInteractionResponseBehavior] Ephemeral acknowledgement of the interaction. - */ - public suspend fun acknowledgeEphemeral(): EphemeralInteractionResponseBehavior { - kord.rest.interaction.acknowledge(id, token, true) - return EphemeralInteractionResponseBehavior(applicationId, token, kord) - } - - /** - * Acknowledges an interaction. - * - * @return [PublicInteractionResponseBehavior] public acknowledgement of an interaction. - */ - public suspend fun acknowledgePublic(): PublicInteractionResponseBehavior { - kord.rest.interaction.acknowledge(id, token) - return PublicInteractionResponseBehavior(applicationId, token, kord) - } - - - public suspend fun getOriginalInteractionResponse(): Message? { - return EntitySupplyStrategy.rest.supply(kord).getOriginalInteractionOrNull(applicationId, token) - } - - override fun withStrategy(strategy: EntitySupplyStrategy<*>): InteractionBehavior = InteractionBehavior(id, channelId, token, applicationId, kord, strategy) - -} - - -/** - * Acknowledges an interaction and responds with [PublicInteractionResponseBehavior]. - * - * @param builder [InteractionResponseCreateBuilder] used to create a public response. - * @return [PublicInteractionResponseBehavior] public response to the interaction. - */ - -@OptIn(ExperimentalContracts::class) -public suspend inline fun InteractionBehavior.respondPublic( - builder: InteractionResponseCreateBuilder.() -> Unit -): PublicInteractionResponseBehavior { - - contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - - val request = InteractionResponseCreateBuilder().apply(builder).toRequest() - kord.rest.interaction.createInteractionResponse(id, token, request) - return PublicInteractionResponseBehavior(applicationId, token, kord) - -} - - -/** - * Acknowledges an interaction and responds with [EphemeralInteractionResponseBehavior] with ephemeral flag. - * - * @param builder [InteractionResponseCreateBuilder] used to a create an ephemeral response. - * @return [InteractionResponseBehavior] ephemeral response to the interaction. - */ - -@OptIn(ExperimentalContracts::class) -public suspend inline fun InteractionBehavior.respondEphemeral( - builder: InteractionResponseCreateBuilder.() -> Unit -): EphemeralInteractionResponseBehavior { - - contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val builder = InteractionResponseCreateBuilder(true).apply(builder) - val request = builder.toRequest() - kord.rest.interaction.createInteractionResponse(id, token, request) - return EphemeralInteractionResponseBehavior(applicationId, token, kord) - -} - -public fun InteractionBehavior( - id: Snowflake, - channelId: Snowflake, - token: String, - applicationId: Snowflake, - kord: Kord, - strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy -): InteractionBehavior = object : InteractionBehavior { - override val id: Snowflake - get() = id - - override val token: String - get() = token - - override val applicationId: Snowflake - get() = applicationId - - override val kord: Kord - get() = kord - - override val channelId: Snowflake - get() = channelId - - - override val supplier: EntitySupplier = strategy.supply(kord) - } diff --git a/core/src/main/kotlin/behavior/interaction/InteractionResponseBehavior.kt b/core/src/main/kotlin/behavior/interaction/InteractionResponseBehavior.kt index 13d4fb9d893e..a335d18e20a5 100644 --- a/core/src/main/kotlin/behavior/interaction/InteractionResponseBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/InteractionResponseBehavior.kt @@ -14,7 +14,7 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** - * The behavior of a [Discord Interaction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) + * The behavior of a [Discord ActionInteraction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) */ public interface InteractionResponseBehavior : KordObject { diff --git a/core/src/main/kotlin/behavior/interaction/PublicInteractionResponseBehavior.kt b/core/src/main/kotlin/behavior/interaction/PublicInteractionResponseBehavior.kt index 6ca64d7c976d..10ac9287ba1e 100644 --- a/core/src/main/kotlin/behavior/interaction/PublicInteractionResponseBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/PublicInteractionResponseBehavior.kt @@ -6,7 +6,7 @@ import dev.kord.rest.request.RestRequestException /** - * The behavior of a public [Discord Interaction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) + * The behavior of a public [Discord ActionInteraction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) * This response is visible to all users in the channel. */ diff --git a/core/src/main/kotlin/cache/data/ApplicationCommandData.kt b/core/src/main/kotlin/cache/data/ApplicationCommandData.kt index 7322a40de155..dc3aa3e32dfa 100644 --- a/core/src/main/kotlin/cache/data/ApplicationCommandData.kt +++ b/core/src/main/kotlin/cache/data/ApplicationCommandData.kt @@ -2,10 +2,17 @@ package dev.kord.core.cache.data import dev.kord.cache.api.data.DataDescription import dev.kord.cache.api.data.description -import dev.kord.common.annotation.KordPreview -import dev.kord.common.entity.* -import dev.kord.common.entity.optional.* -import dev.kord.core.entity.application.ApplicationCommand +import dev.kord.common.entity.ApplicationCommandOption +import dev.kord.common.entity.ApplicationCommandOptionType +import dev.kord.common.entity.ApplicationCommandType +import dev.kord.common.entity.Choice +import dev.kord.common.entity.DiscordApplicationCommand +import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.OptionalBoolean +import dev.kord.common.entity.optional.OptionalSnowflake +import dev.kord.common.entity.optional.mapList +import dev.kord.common.entity.optional.orEmpty import kotlinx.serialization.Serializable @Serializable diff --git a/core/src/main/kotlin/cache/data/InteractionData.kt b/core/src/main/kotlin/cache/data/InteractionData.kt index edbe981376a8..ea0dc7f1195a 100644 --- a/core/src/main/kotlin/cache/data/InteractionData.kt +++ b/core/src/main/kotlin/cache/data/InteractionData.kt @@ -1,9 +1,21 @@ package dev.kord.core.cache.data import dev.kord.common.annotation.KordExperimental -import dev.kord.common.annotation.KordPreview -import dev.kord.common.entity.* +import dev.kord.common.entity.ApplicationCommandType +import dev.kord.common.entity.Choice +import dev.kord.common.entity.CommandArgument +import dev.kord.common.entity.CommandGroup +import dev.kord.common.entity.ComponentType +import dev.kord.common.entity.DiscordInteraction +import dev.kord.common.entity.InteractionCallbackData +import dev.kord.common.entity.InteractionType +import dev.kord.common.entity.Option +import dev.kord.common.entity.Permissions +import dev.kord.common.entity.ResolvedObjects +import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.SubCommand import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.common.entity.optional.OptionalSnowflake import dev.kord.common.entity.optional.flatMap import dev.kord.common.entity.optional.map @@ -119,13 +131,14 @@ public data class OptionData( @OptIn(KordExperimental::class) val value: Optional> = Optional.Missing(), val values: Optional>> = Optional.Missing(), - val subCommands: Optional> = Optional.Missing() + val subCommands: Optional> = Optional.Missing(), + val focused: OptionalBoolean = OptionalBoolean.Missing ) { public companion object { public fun from(data: Option): OptionData = with(data) { when (data) { is SubCommand -> OptionData(name, values = data.options) - is CommandArgument<*> -> OptionData(name, value = Optional(data)) + is CommandArgument<*> -> OptionData(name, value = Optional(data), focused = data.focused) is CommandGroup -> OptionData(name, subCommands = data.options) } } diff --git a/core/src/main/kotlin/entity/Message.kt b/core/src/main/kotlin/entity/Message.kt index 20630b351850..0284f8c01f31 100644 --- a/core/src/main/kotlin/entity/Message.kt +++ b/core/src/main/kotlin/entity/Message.kt @@ -11,19 +11,26 @@ import dev.kord.core.behavior.MessageBehavior import dev.kord.core.behavior.UserBehavior import dev.kord.core.behavior.channel.ChannelBehavior import dev.kord.core.cache.data.MessageData -import dev.kord.core.entity.channel.* +import dev.kord.core.entity.channel.Channel +import dev.kord.core.entity.channel.GuildChannel +import dev.kord.core.entity.channel.MessageChannel +import dev.kord.core.entity.channel.TopGuildMessageChannel import dev.kord.core.entity.component.Component -import dev.kord.core.entity.interaction.Interaction +import dev.kord.core.entity.interaction.ActionInteraction import dev.kord.core.entity.interaction.MessageInteraction import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant import kotlinx.datetime.toInstant -import java.util.* +import java.util.Objects /** * An instance of a [Discord Message][https://discord.com/developers/docs/resources/channel#message-object]. @@ -105,7 +112,7 @@ public class Message( public val stickers: List get() = data.stickers.orEmpty().map { MessageSticker(it, kord) } /** - * If the message is a response to an [Interaction], this is the id of the interaction's application + * If the message is a response to an [ActionInteraction], this is the id of the interaction's application */ public val applicationId: Snowflake? get() = data.application.unwrap { it.id } @@ -182,7 +189,7 @@ public class Message( public val mentionedUserBehaviors: Set get() = data.mentions.map { UserBehavior(it, kord) }.toSet() /** - * The [MessageInteraction] sent on this message object when it is a response to an [dev.kord.core.entity.interaction.Interaction]. + * The [MessageInteraction] sent on this message object when it is a response to an [dev.kord.core.entity.interaction.ActionInteraction]. */ public val interaction: MessageInteraction? get() = data.interaction.mapNullable { MessageInteraction(it, kord) }.value diff --git a/core/src/main/kotlin/entity/application/ChatInputCommandCommand.kt b/core/src/main/kotlin/entity/application/ChatInputCommandCommand.kt index 5dd5a6c64b06..5c61b74b367b 100644 --- a/core/src/main/kotlin/entity/application/ChatInputCommandCommand.kt +++ b/core/src/main/kotlin/entity/application/ChatInputCommandCommand.kt @@ -15,8 +15,8 @@ import dev.kord.rest.service.InteractionService public sealed interface ChatInputCommandCommand : ApplicationCommand, ChatInputCommandBehavior { - public val description: String - get() = data.description!! + public val description: String? + get() = data.description /** * The groups of this command, each group contains at least one [sub command][ChatInputSubCommand]. */ diff --git a/core/src/main/kotlin/entity/interaction/ActionInteraction.kt b/core/src/main/kotlin/entity/interaction/ActionInteraction.kt new file mode 100644 index 000000000000..700a2015350e --- /dev/null +++ b/core/src/main/kotlin/entity/interaction/ActionInteraction.kt @@ -0,0 +1,433 @@ +package dev.kord.core.entity.interaction + +import dev.kord.common.entity.ApplicationCommandType +import dev.kord.common.entity.CommandArgument +import dev.kord.common.entity.Permissions +import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.mapValues +import dev.kord.common.entity.optional.orEmpty +import dev.kord.common.entity.optional.unwrap +import dev.kord.core.Kord +import dev.kord.core.KordObject +import dev.kord.core.behavior.GuildInteractionBehavior +import dev.kord.core.behavior.MemberBehavior +import dev.kord.core.behavior.UserBehavior +import dev.kord.core.behavior.channel.GuildMessageChannelBehavior +import dev.kord.core.behavior.interaction.ActionInteractionBehavior +import dev.kord.core.cache.data.ApplicationInteractionData +import dev.kord.core.cache.data.InteractionData +import dev.kord.core.cache.data.ResolvedObjectsData +import dev.kord.core.entity.Entity +import dev.kord.core.entity.Member +import dev.kord.core.entity.Message +import dev.kord.core.entity.Role +import dev.kord.core.entity.User +import dev.kord.core.entity.application.GlobalApplicationCommand +import dev.kord.core.entity.channel.ResolvedChannel +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy + +/** + * An instance of [ActionInteraction](https://discord.com/developers/docs/interactions/slash-commands#interaction) which does perform an action + * (e.g. slash commands and context actions). + * + * @see DataInteraction + * @see Interaction + */ +public sealed interface ActionInteraction : Interaction, ActionInteractionBehavior + +/** + * The base interaction for all slash-command related interactions. + * + * @see GuildApplicationCommandInteraction + */ +public sealed class CommandInteraction : ActionInteraction { + public val command: InteractionCommand + get() = InteractionCommand(data.data, kord) +} + +/** + * The base command of all commands that can be executed under an interaction event. + */ +public sealed interface InteractionCommand : KordObject { + /** + * The id of the root command. + */ + public val rootId: Snowflake + + /** + * The root command name + */ + public val rootName: String + + /** + * the values passed to the command. + */ + public val options: Map> + + public val resolved: ResolvedObjects? + + public val strings: Map get() = filterOptions() + + public val integers: Map get() = filterOptions() + + public val numbers: Map get() = filterOptions() + + public val booleans: Map get() = filterOptions() + + public val users: Map get() = filterOptions() + + public val members: Map get() = filterOptions() + + public val channels: Map get() = filterOptions() + + public val roles: Map get() = filterOptions() + + public val mentionables: Map get() = filterOptions() + + private inline fun filterOptions(): Map { + return buildMap { + options.onEach { (key, value) -> + val wrappedValue = value.value + if (wrappedValue is T) put(key, wrappedValue) + } + } + } +} + +public fun InteractionCommand( + data: ApplicationInteractionData, + kord: Kord +): InteractionCommand { + val firstLevelOptions = data.options.orEmpty() + val rootPredicate = firstLevelOptions.isEmpty() || firstLevelOptions.any { it.value.value != null } + val groupPredicate = firstLevelOptions.any { it.subCommands.orEmpty().isNotEmpty() } + val subCommandPredicate = + firstLevelOptions.all { it.value is Optional.Missing && it.subCommands is Optional.Missing } + + return when { + rootPredicate -> RootCommand(data, kord) + groupPredicate -> GroupCommand(data, kord) + subCommandPredicate -> SubCommand(data, kord) + else -> error("The interaction data provided is not an chat input command") + } +} + +/** + * Represents an invocation of a root command. + * + * The root command is the first command defined in a slash-command structure. + */ + +public class RootCommand( + public val data: ApplicationInteractionData, + override val kord: Kord +) : InteractionCommand { + + override val rootId: Snowflake + get() = data.id.value!! + + override val rootName: String get() = data.name.value!! + + override val options: Map> + get() = data.options.orEmpty() + .associate { it.name to OptionValue(it.value.value!!, resolved) } + + override val resolved: ResolvedObjects? + get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } + +} + +/** + * Represents an invocation of a sub-command under the [RootCommand] + */ + +public class SubCommand( + public val data: ApplicationInteractionData, + override val kord: Kord +) : InteractionCommand { + + private val subCommandData = data.options.orEmpty().first() + + override val rootName: String get() = data.name.value!! + + override val rootId: Snowflake + get() = data.id.value!! + + /** + * Name of the sub-command executed. + */ + public val name: String get() = subCommandData.name + + override val options: Map> + get() = subCommandData.values.orEmpty() + .associate { it.name to OptionValue(it, resolved) } + + + override val resolved: ResolvedObjects? + get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } + + +} + +/** + * Represents an invocation of a sub-command under a group. + */ + +public class GroupCommand( + public val data: ApplicationInteractionData, + override val kord: Kord +) : InteractionCommand { + + private val groupData get() = data.options.orEmpty().first() + private val subCommandData get() = groupData.subCommands.orEmpty().first() + + override val rootId: Snowflake + get() = data.id.value!! + + override val rootName: String get() = data.name.value!! + + /** + * Name of the group of this sub-command. + */ + public val groupName: String get() = groupData.name + + /** + * Name of this sub-command + */ + public val name: String get() = subCommandData.name + + override val options: Map> + get() = subCommandData.options.orEmpty() + .associate { it.name to OptionValue(it, resolved) } + + + override val resolved: ResolvedObjects? + get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } + +} + + +public class ResolvedObjects( + public val data: ResolvedObjectsData, + public val kord: Kord, + public val strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy +) { + public val channels: Map? + get() = data.channels.mapValues { ResolvedChannel(it.value, kord, strategy) }.value + + public val roles: Map? get() = data.roles.mapValues { Role(it.value, kord) }.value + + public val users: Map? get() = data.users.mapValues { User(it.value, kord) }.value + + public val members: Map? + get() = data.members.mapValues { Member(it.value, users!![it.key]!!.data, kord) }.value + + public val messages: Map? + get() = data.messages.mapValues { Message(it.value, kord) }.value + +} + + +public sealed class OptionValue(public val value: T, public val focused: Boolean) { + + public class RoleOptionValue(value: Role, focused: Boolean) : OptionValue(value, focused) { + override fun toString(): String = "RoleOptionValue(value=$value)" + } + + public open class UserOptionValue(value: User, focused: Boolean) : OptionValue(value, focused) { + override fun toString(): String = "UserOptionValue(value=$value)" + } + + public class MemberOptionValue(value: Member, focused: Boolean) : UserOptionValue(value, focused) { + override fun toString(): String = "MemberOptionValue(value=$value)" + } + + public class ChannelOptionValue(value: ResolvedChannel, focused: Boolean) : + OptionValue(value, focused) { + override fun toString(): String = "ChannelOptionValue(value=$value)" + } + + public class IntOptionValue(value: Long, focused: Boolean) : OptionValue(value, focused) { + override fun toString(): String = "IntOptionValue(value=$value)" + } + + + public class NumberOptionValue(value: Double, focused: Boolean) : OptionValue(value, focused) { + override fun toString(): String = "DoubleOptionValue(value=$value)" + } + + public class StringOptionValue(value: String, focused: Boolean) : OptionValue(value, focused) { + override fun toString(): String = "StringOptionValue(value=$value)" + } + + public class BooleanOptionValue(value: Boolean, focused: Boolean) : OptionValue(value, focused) { + override fun toString(): String = "BooleanOptionValue(value=$value)" + } + + public class MentionableOptionValue(value: Entity, focused: Boolean) : OptionValue(value, focused) { + override fun toString(): String = "MentionableOptionValue(value=$value)" + } + +} + + +public fun OptionValue(value: CommandArgument<*>, resolvedObjects: ResolvedObjects?): OptionValue<*> { + val focused = value.focused.orElse(false) + return when (value) { + is CommandArgument.NumberArgument -> OptionValue.NumberOptionValue(value.value, focused) + is CommandArgument.BooleanArgument -> OptionValue.BooleanOptionValue(value.value, focused) + is CommandArgument.IntegerArgument -> OptionValue.IntOptionValue(value.value, focused) + is CommandArgument.StringArgument -> OptionValue.StringOptionValue(value.value, focused) + is CommandArgument.ChannelArgument -> { + val channel = resolvedObjects?.channels.orEmpty()[value.value] + requireNotNull(channel) { "channel expected for $value but was missing" } + + OptionValue.ChannelOptionValue(channel, focused) + } + + is CommandArgument.MentionableArgument -> { + val channel = resolvedObjects?.channels.orEmpty()[value.value] + val user = resolvedObjects?.users.orEmpty()[value.value] + val member = resolvedObjects?.members.orEmpty()[value.value] + val role = resolvedObjects?.roles.orEmpty()[value.value] + + val entity = channel ?: member ?: user ?: role + requireNotNull(entity) { "user, member, or channel expected for $value but was missing" } + + OptionValue.MentionableOptionValue(entity, focused) + } + + is CommandArgument.RoleArgument -> { + val role = resolvedObjects?.roles.orEmpty()[value.value] + requireNotNull(role) { "role expected for $value but was missing" } + + OptionValue.RoleOptionValue(role, focused) + } + + is CommandArgument.UserArgument -> { + val member = resolvedObjects?.members.orEmpty()[value.value] + + if (member != null) return OptionValue.MemberOptionValue(member, focused) + + val user = resolvedObjects?.users.orEmpty()[value.value] + requireNotNull(user) { "user expected for $value but was missing" } + + OptionValue.UserOptionValue(user, focused) + } + } +} + + +public sealed interface GlobalInteraction : Interaction { + override val user: User get() = User(data.user.value!!, kord) +} + +/** + * An [ActionInteraction] that took place in a Global Context with [GlobalApplicationCommand]. + */ +public sealed interface GlobalApplicationCommandInteraction : ApplicationCommandInteraction, GlobalInteraction { + /** + * The user who invoked the interaction. + */ + override fun withStrategy(strategy: EntitySupplyStrategy<*>): GlobalApplicationCommandInteraction = + GlobalApplicationCommandInteraction(data, kord, strategy.supply(kord)) + + override val applicationId: Snowflake + get() = super.applicationId +} + +public fun GlobalApplicationCommandInteraction( + data: InteractionData, + kord: Kord, + supplier: EntitySupplier = kord.defaultSupplier +): GlobalApplicationCommandInteraction { + return when (data.data.type.value) { + ApplicationCommandType.ChatInput -> GlobalChatInputCommandInteraction(data, kord, supplier) + ApplicationCommandType.User -> GlobalUserCommandInteraction(data, kord, supplier) + ApplicationCommandType.Message -> GlobalMessageCommandInteraction(data, kord, supplier) + is ApplicationCommandType.Unknown -> error("Unknown interaction.") + null -> error("No component type was provided") + } +} + +/** + * An [ActionInteraction] that took place in a Global Context with [dev.kord.core.entity.application.GuildApplicationCommand]. + */ + + +public sealed interface GuildApplicationCommandInteraction : ApplicationCommandInteraction, GuildInteractionBehavior { + + override val guildId: Snowflake + get() = data.guildId.value!! + + /** + * Overridden permissions of the interaction invoker in the channel. + */ + public val permissions: Permissions get() = data.permissions.value!! + + + /** + * The invoker of the command as [MemberBehavior]. + */ + public val member: Member + get() = Member( + data.member.value!!, + data.user.value!!, + kord + ) + + override val channel: GuildMessageChannelBehavior + get() = GuildMessageChannelBehavior(guildId, channelId, kord) + + override val user: UserBehavior + get() = UserBehavior(member.id, kord) + + override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildApplicationCommandInteraction = + GuildApplicationCommandInteraction(data, kord, strategy.supply(kord)) + +} + +public fun GuildApplicationCommandInteraction( + data: InteractionData, + kord: Kord, + supplier: EntitySupplier = kord.defaultSupplier +): GuildApplicationCommandInteraction { + return when (data.data.type.value) { + ApplicationCommandType.ChatInput -> GuildChatInputCommandInteraction(data, kord, supplier) + ApplicationCommandType.User -> GuildUserCommandInteraction(data, kord, supplier) + ApplicationCommandType.Message -> GuildMessageCommandInteraction(data, kord, supplier) + is ApplicationCommandType.Unknown -> error("Unknown interaction.") + null -> error("No interaction type provided.") + } +} + + +public fun OptionValue<*>.user(): User = value as User + + +public fun OptionValue<*>.channel(): ResolvedChannel = value as ResolvedChannel + + +public fun OptionValue<*>.role(): Role = value as Role + + +public fun OptionValue<*>.member(): Member = value as Member + + +public fun OptionValue<*>.string(): String = value.toString() + + +public fun OptionValue<*>.boolean(): Boolean = value as Boolean + + +public fun OptionValue<*>.int(): Long = value as Long + + +public fun OptionValue<*>.number(): Double = value as Double + + +public fun OptionValue<*>.mentionable(): Entity { + return value as Entity +} diff --git a/core/src/main/kotlin/entity/interaction/ComponentInteraction.kt b/core/src/main/kotlin/entity/interaction/ComponentInteraction.kt index e82395792e52..af2df5ee7954 100644 --- a/core/src/main/kotlin/entity/interaction/ComponentInteraction.kt +++ b/core/src/main/kotlin/entity/interaction/ComponentInteraction.kt @@ -22,7 +22,7 @@ import dev.kord.rest.builder.component.SelectMenuBuilder * @see SelectMenuInteraction */ -public sealed interface ComponentInteraction : Interaction, ComponentInteractionBehavior { +public sealed interface ComponentInteraction : ActionInteraction, ComponentInteractionBehavior { override val user: User get() = User(data.user.value!!, kord) diff --git a/core/src/main/kotlin/entity/interaction/ContextInteraction.kt b/core/src/main/kotlin/entity/interaction/ContextInteraction.kt index 5a02db9237e3..c507b4980efc 100644 --- a/core/src/main/kotlin/entity/interaction/ContextInteraction.kt +++ b/core/src/main/kotlin/entity/interaction/ContextInteraction.kt @@ -2,17 +2,20 @@ package dev.kord.core.entity.interaction import dev.kord.common.entity.ApplicationCommandType import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.OptionalSnowflake import dev.kord.common.entity.optional.unwrap import dev.kord.core.Kord +import dev.kord.core.behavior.GuildInteractionBehavior 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] @@ -28,17 +31,22 @@ public sealed interface ApplicationCommandInteraction : Interaction, Application get() = data.data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } - } /** - * An [ApplicationCommandInteraction] that's invoked through chat input. + * An [ApplicationCommandInteraction] that contains a [command]. */ -public sealed interface ChatInputCommandInteraction : ApplicationCommandInteraction { +public sealed interface ChatInputCommandInteraction : Interaction { public val command: InteractionCommand get() = InteractionCommand(data.data, kord) } +/** + * An [ApplicationCommandInteraction] that's invoked through chat input. + */ +public sealed interface ChatInputCommandInvocationInteraction : ChatInputCommandInteraction, ApplicationCommandInteraction + + /** * A [ApplicationCommandInteraction] that's invoked through chat input specific to a guild. */ @@ -46,7 +54,7 @@ public class GuildChatInputCommandInteraction( override val data: InteractionData, override val kord: Kord, override val supplier: EntitySupplier -) : ChatInputCommandInteraction, GuildApplicationCommandInteraction { +) : ChatInputCommandInvocationInteraction, GuildApplicationCommandInteraction { override fun equals(other: Any?): Boolean { return if (other !is GuildChatInputCommandInteraction) false else id == other.id @@ -65,7 +73,7 @@ public class GlobalChatInputCommandInteraction( override val data: InteractionData, override val kord: Kord, override val supplier: EntitySupplier -) : ChatInputCommandInteraction, GlobalApplicationCommandInteraction { +) : ChatInputCommandInvocationInteraction, GlobalApplicationCommandInteraction { override fun equals(other: Any?): Boolean { return if (other !is GlobalChatInputCommandInteraction) false else id == other.id @@ -141,7 +149,7 @@ public sealed interface MessageCommandInteraction : ApplicationCommandInteractio public suspend fun getTarget(): Message = supplier.getMessage(channelId, targetId) - public suspend fun getTargetOrNull(): Message? = supplier.getMessageOrNull(channelId, targetId) + public suspend fun getTargetOrNull(): Message? = supplier.getMessageOrNull(channelId, targetId) public val messages: Map get() = resolvedObjects!!.messages!! @@ -205,3 +213,60 @@ public class UnknownApplicationCommandInteraction( return UnknownApplicationCommandInteraction(data, kord, strategy.supply(kord)) } } + +/** + * ActionInteraction indicating an auto-complete request from Discord. + * + * **Follow-ups and normals responses don't work on this type** + * + * Check [AutoCompleteInteractionBehavior] for response options + */ +public sealed interface AutoCompleteInteraction : AutoCompleteInteractionBehavior, ChatInputCommandInteraction, DataInteraction + +internal fun AutoCompleteInteraction( + data: InteractionData, + kord: Kord, + supplier: EntitySupplier = kord.defaultSupplier +): AutoCompleteInteraction = when (data.guildId) { + is OptionalSnowflake.Value -> GuildAutoCompleteInteraction( + data, kord, supplier + ) + else -> GlobalAutoCompleteInteraction(data, kord, supplier) +} + +/** + * ActionInteraction indicating an auto-complete request from Discord. + * + * **Follow-ups and normals responses don't work on this type** + * + * @see ApplicationCommandInteraction + */ +public class GlobalAutoCompleteInteraction( + override val data: InteractionData, + override val kord: Kord, + override val supplier: EntitySupplier = kord.defaultSupplier +) : AutoCompleteInteraction, GlobalInteraction { + override fun withStrategy(strategy: EntitySupplyStrategy<*>): GlobalAutoCompleteInteraction = + GlobalAutoCompleteInteraction(data, kord, strategy.supply(kord)) +} + +/** + * ActionInteraction indicating an auto-complete request from Discord on a guild. + * + * **Follow-ups and normals responses don't work on this type** + * + * @see ApplicationCommandInteraction + */ +public class GuildAutoCompleteInteraction( + override val data: InteractionData, + override val kord: Kord, + override val supplier: EntitySupplier = kord.defaultSupplier +) : AutoCompleteInteraction, GuildInteractionBehavior { + override val guildId: Snowflake + get() = data.guildId.value!! + + override val user: User get() = User(data.user.value!!, kord) + + override fun withStrategy(strategy: EntitySupplyStrategy<*>): Interaction = + GuildAutoCompleteInteraction(data, kord, strategy.supply(kord)) +} diff --git a/core/src/main/kotlin/entity/interaction/DataInteraction.kt b/core/src/main/kotlin/entity/interaction/DataInteraction.kt new file mode 100644 index 000000000000..b5b83f53d0e9 --- /dev/null +++ b/core/src/main/kotlin/entity/interaction/DataInteraction.kt @@ -0,0 +1,10 @@ +package dev.kord.core.entity.interaction + +/** + * An instance of [ActionInteraction](https://discord.com/developers/docs/interactions/slash-commands#interaction) which does respond to a data request from + * Discord like [AutoCompleteInteractions][AutoCompleteInteraction]. + * + * @see ActionInteraction + * @see Interaction + */ +public sealed interface DataInteraction : Interaction diff --git a/core/src/main/kotlin/entity/interaction/Interaction.kt b/core/src/main/kotlin/entity/interaction/Interaction.kt index 1d3288296d1d..dba5f698963e 100644 --- a/core/src/main/kotlin/entity/interaction/Interaction.kt +++ b/core/src/main/kotlin/entity/interaction/Interaction.kt @@ -1,28 +1,21 @@ package dev.kord.core.entity.interaction -import dev.kord.common.entity.* -import dev.kord.common.entity.optional.* +import dev.kord.common.entity.InteractionType +import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.optional.OptionalSnowflake import dev.kord.core.Kord -import dev.kord.core.KordObject -import dev.kord.core.behavior.* -import dev.kord.core.behavior.channel.GuildMessageChannelBehavior +import dev.kord.core.behavior.UserBehavior import dev.kord.core.behavior.interaction.InteractionBehavior -import dev.kord.core.cache.data.ApplicationInteractionData import dev.kord.core.cache.data.InteractionData -import dev.kord.core.cache.data.ResolvedObjectsData -import dev.kord.core.entity.* -import dev.kord.core.entity.application.GlobalApplicationCommand -import dev.kord.core.entity.channel.ResolvedChannel -import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy -import dev.kord.rest.service.InteractionService /** - * An instance of [Interaction] (https://discord.com/developers/docs/interactions/slash-commands#interaction) + * An instance of [ActionInteraction](https://discord.com/developers/docs/interactions/slash-commands#interaction). + * + * @see ActionInteraction + * @see DataInteraction */ - public sealed interface Interaction : InteractionBehavior { - public val data: InteractionData override val id: Snowflake get() = data.id @@ -62,6 +55,7 @@ public sealed interface Interaction : InteractionBehavior { ): Interaction { return when { data.type == InteractionType.Component -> ComponentInteraction(data, kord, strategy.supply(kord)) + data.type == InteractionType.AutoComplete -> AutoCompleteInteraction(data, kord, strategy.supply(kord)) data.guildId !is OptionalSnowflake.Missing -> GuildApplicationCommandInteraction( data, kord, @@ -71,405 +65,4 @@ public sealed interface Interaction : InteractionBehavior { } } } - -} - -/** - * The base interaction for all slash-command related interactions. - * - * @see DmInteraction - * @see GuildApplicationCommandInteraction - */ - -public sealed class CommandInteraction : Interaction { - public val command: InteractionCommand - get() = InteractionCommand(data.data, kord) -} - -/** - * The base command of all commands that can be executed under an interaction event. - */ - -public sealed interface InteractionCommand : KordObject { - /** - * The id of the root command. - */ - public val rootId: Snowflake - - /** - * The root command name - */ - public val rootName: String - - /** - * the values passed to the command. - */ - public val options: Map> - - public val resolved: ResolvedObjects? - - public val strings: Map get() = filterOptions() - - public val integers: Map get() = filterOptions() - - public val numbers: Map get() = filterOptions() - - public val booleans: Map get() = filterOptions() - - public val users: Map get() = filterOptions() - - public val members: Map get() = filterOptions() - - public val channels: Map get() = filterOptions() - - public val roles: Map get() = filterOptions() - - public val mentionables: Map get() = filterOptions() - - private inline fun filterOptions(): Map { - return buildMap { - options.onEach { (key, value) -> - val wrappedValue = value.value - if (wrappedValue is T) put(key, wrappedValue) - } - } - } -} - -public fun InteractionCommand( - data: ApplicationInteractionData, - kord: Kord -): InteractionCommand { - val firstLevelOptions = data.options.orEmpty() - val rootPredicate = firstLevelOptions.isEmpty() || firstLevelOptions.any { it.value.value != null } - val groupPredicate = firstLevelOptions.any { it.subCommands.orEmpty().isNotEmpty() } - val subCommandPredicate = - firstLevelOptions.all { it.value is Optional.Missing && it.subCommands is Optional.Missing } - - return when { - rootPredicate -> RootCommand(data, kord) - groupPredicate -> GroupCommand(data, kord) - subCommandPredicate -> SubCommand(data, kord) - else -> error("The interaction data provided is not an chat input command") - } -} - -/** - * Represents an invocation of a root command. - * - * The root command is the first command defined in a slash-command structure. - */ - -public class RootCommand( - public val data: ApplicationInteractionData, - override val kord: Kord -) : InteractionCommand { - - override val rootId: Snowflake - get() = data.id.value!! - - override val rootName: String get() = data.name.value!! - - override val options: Map> - get() = data.options.orEmpty() - .associate { it.name to OptionValue(it.value.value!!, resolved) } - - override val resolved: ResolvedObjects? - get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } - -} - -/** - * Represents an invocation of a sub-command under the [RootCommand] - */ - -public class SubCommand( - public val data: ApplicationInteractionData, - override val kord: Kord -) : InteractionCommand { - - private val subCommandData = data.options.orEmpty().first() - - override val rootName: String get() = data.name.value!! - - override val rootId: Snowflake - get() = data.id.value!! - - /** - * Name of the sub-command executed. - */ - public val name: String get() = subCommandData.name - - override val options: Map> - get() = subCommandData.values.orEmpty() - .associate { it.name to OptionValue(it, resolved) } - - - override val resolved: ResolvedObjects? - get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } - - -} - -/** - * Represents an invocation of a sub-command under a group. - */ - -public class GroupCommand( - public val data: ApplicationInteractionData, - override val kord: Kord -) : InteractionCommand { - - private val groupData get() = data.options.orEmpty().first() - private val subCommandData get() = groupData.subCommands.orEmpty().first() - - override val rootId: Snowflake - get() = data.id.value!! - - override val rootName: String get() = data.name.value!! - - /** - * Name of the group of this sub-command. - */ - public val groupName: String get() = groupData.name - - /** - * Name of this sub-command - */ - public val name: String get() = subCommandData.name - - override val options: Map> - get() = subCommandData.options.orEmpty() - .associate { it.name to OptionValue(it, resolved) } - - - override val resolved: ResolvedObjects? - get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } - -} - - -public class ResolvedObjects( - public val data: ResolvedObjectsData, - public val kord: Kord, - public val strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy -) { - public val channels: Map? - get() = data.channels.mapValues { ResolvedChannel(it.value, kord, strategy) }.value - - public val roles: Map? get() = data.roles.mapValues { Role(it.value, kord) }.value - - public val users: Map? get() = data.users.mapValues { User(it.value, kord) }.value - - public val members: Map? - get() = data.members.mapValues { Member(it.value, users!![it.key]!!.data, kord) }.value - - public val messages: Map? - get() = data.messages.mapValues { Message(it.value, kord) }.value - -} - - -public sealed class OptionValue(public val value: T) { - - public class RoleOptionValue(value: Role) : OptionValue(value) { - override fun toString(): String = "RoleOptionValue(value=$value)" - } - - public open class UserOptionValue(value: User) : OptionValue(value) { - override fun toString(): String = "UserOptionValue(value=$value)" - } - - public class MemberOptionValue(value: Member) : UserOptionValue(value) { - override fun toString(): String = "MemberOptionValue(value=$value)" - } - - public class ChannelOptionValue(value: ResolvedChannel) : OptionValue(value) { - override fun toString(): String = "ChannelOptionValue(value=$value)" - } - - public class IntOptionValue(value: Long) : OptionValue(value) { - override fun toString(): String = "IntOptionValue(value=$value)" - } - - - public class NumberOptionValue(value: Double) : OptionValue(value) { - override fun toString(): String = "DoubleOptionValue(value=$value)" - } - - public class StringOptionValue(value: String) : OptionValue(value) { - override fun toString(): String = "StringOptionValue(value=$value)" - } - - public class BooleanOptionValue(value: Boolean) : OptionValue(value) { - override fun toString(): String = "BooleanOptionValue(value=$value)" - } - - public class MentionableOptionValue(value: Entity) : OptionValue(value) { - override fun toString(): String = "MentionableOptionValue(value=$value)" - } - -} - - -public fun OptionValue(value: CommandArgument<*>, resolvedObjects: ResolvedObjects?): OptionValue<*> { - return when (value) { - is CommandArgument.NumberArgument -> OptionValue.NumberOptionValue(value.value) - is CommandArgument.BooleanArgument -> OptionValue.BooleanOptionValue(value.value) - is CommandArgument.IntegerArgument -> OptionValue.IntOptionValue(value.value) - is CommandArgument.StringArgument -> OptionValue.StringOptionValue(value.value) - is CommandArgument.ChannelArgument -> { - val channel = resolvedObjects?.channels.orEmpty()[value.value] - requireNotNull(channel) { "channel expected for $value but was missing" } - - OptionValue.ChannelOptionValue(channel) - } - - is CommandArgument.MentionableArgument -> { - val channel = resolvedObjects?.channels.orEmpty()[value.value] - val user = resolvedObjects?.users.orEmpty()[value.value] - val member = resolvedObjects?.members.orEmpty()[value.value] - val role = resolvedObjects?.roles.orEmpty()[value.value] - - val entity = channel ?: member ?: user ?: role - requireNotNull(entity) { "user, member, or channel expected for $value but was missing" } - - OptionValue.MentionableOptionValue(entity) - } - - is CommandArgument.RoleArgument -> { - val role = resolvedObjects?.roles.orEmpty()[value.value] - requireNotNull(role) { "role expected for $value but was missing" } - - OptionValue.RoleOptionValue(role) - } - - is CommandArgument.UserArgument -> { - val member = resolvedObjects?.members.orEmpty()[value.value] - - if (member != null) return OptionValue.MemberOptionValue(member) - - val user = resolvedObjects?.users.orEmpty()[value.value] - requireNotNull(user) { "user expected for $value but was missing" } - - OptionValue.UserOptionValue(user) - } - } -} - - -/** - * An [Interaction] that took place in a Global Context with [GlobalApplicationCommand]. - */ - -public sealed interface GlobalApplicationCommandInteraction : ApplicationCommandInteraction, - GlobalApplicationCommandBehavior { - /** - * The user who invoked the interaction. - */ - - override val service: InteractionService - get() = kord.rest.interaction - - override val user: User get() = User(data.user.value!!, kord) - - override fun withStrategy(strategy: EntitySupplyStrategy<*>): GlobalApplicationCommandInteraction = - GlobalApplicationCommandInteraction(data, kord, strategy.supply(kord)) - - override val applicationId: Snowflake - get() = super.applicationId -} - -public fun GlobalApplicationCommandInteraction( - data: InteractionData, - kord: Kord, - supplier: EntitySupplier = kord.defaultSupplier -): GlobalApplicationCommandInteraction { - return when (data.data.type.value) { - ApplicationCommandType.ChatInput -> GlobalChatInputCommandInteraction(data, kord, supplier) - ApplicationCommandType.User -> GlobalUserCommandInteraction(data, kord, supplier) - ApplicationCommandType.Message -> GlobalMessageCommandInteraction(data, kord, supplier) - is ApplicationCommandType.Unknown -> error("Unknown interaction.") - null -> error("No component type was provided") - } -} - -/** - * An [Interaction] that took place in a Global Context with [dev.kord.core.entity.application.GuildApplicationCommand]. - */ - - -public sealed interface GuildApplicationCommandInteraction : ApplicationCommandInteraction, GuildInteractionBehavior { - - override val guildId: Snowflake - get() = data.guildId.value!! - - /** - * Overridden permissions of the interaction invoker in the channel. - */ - public val permissions: Permissions get() = data.permissions.value!! - - - /** - * The invoker of the command as [MemberBehavior]. - */ - public val member: Member - get() = Member( - data.member.value!!, - data.user.value!!, - kord - ) - - override val channel: GuildMessageChannelBehavior - get() = GuildMessageChannelBehavior(guildId, channelId, kord) - - override val user: UserBehavior - get() = UserBehavior(member.id, kord) - - override fun withStrategy(strategy: EntitySupplyStrategy<*>): GuildApplicationCommandInteraction = - GuildApplicationCommandInteraction(data, kord, strategy.supply(kord)) - -} - -public fun GuildApplicationCommandInteraction( - data: InteractionData, - kord: Kord, - supplier: EntitySupplier = kord.defaultSupplier -): GuildApplicationCommandInteraction { - return when (data.data.type.value) { - ApplicationCommandType.ChatInput -> GuildChatInputCommandInteraction(data, kord, supplier) - ApplicationCommandType.User -> GuildUserCommandInteraction(data, kord, supplier) - ApplicationCommandType.Message -> GuildMessageCommandInteraction(data, kord, supplier) - is ApplicationCommandType.Unknown -> error("Unknown interaction.") - null -> error("No interaction type provided.") - } -} - - -public fun OptionValue<*>.user(): User = value as User - - -public fun OptionValue<*>.channel(): ResolvedChannel = value as ResolvedChannel - - -public fun OptionValue<*>.role(): Role = value as Role - - -public fun OptionValue<*>.member(): Member = value as Member - - -public fun OptionValue<*>.string(): String = value.toString() - - -public fun OptionValue<*>.boolean(): Boolean = value as Boolean - - -public fun OptionValue<*>.int(): Long = value as Long - - -public fun OptionValue<*>.number(): Double = value as Double - - -public fun OptionValue<*>.mentionable(): Entity { - return value as Entity } diff --git a/core/src/main/kotlin/entity/interaction/MessageInteraction.kt b/core/src/main/kotlin/entity/interaction/MessageInteraction.kt index bc86eb6f472c..ce55ed959e78 100644 --- a/core/src/main/kotlin/entity/interaction/MessageInteraction.kt +++ b/core/src/main/kotlin/entity/interaction/MessageInteraction.kt @@ -10,13 +10,14 @@ import dev.kord.core.entity.KordEntity import dev.kord.core.entity.Message import dev.kord.core.entity.Strategizable import dev.kord.core.entity.User +import dev.kord.core.entity.application.ApplicationCommand import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy /** * An instance of [MessageInteraction](https://discord.com/developers/docs/interactions/slash-commands#messageinteraction) - * This is sent on the [Message] object when the message is a response to an [Interaction]. + * This is sent on the [Message] object when the message is a response to an [ActionInteraction]. */ public class MessageInteraction( @@ -25,7 +26,7 @@ public class MessageInteraction( override val supplier: EntitySupplier = kord.defaultSupplier ) : KordEntity, Strategizable { /** - * [id][Interaction.id] of the [Interaction] this message is responding to. + * [id][ActionInteraction.id] of the [ActionInteraction] this message is responding to. */ override val id: Snowflake get() = data.id @@ -35,7 +36,7 @@ public class MessageInteraction( public val name: String get() = data.name /** - * The [UserBehavior] of the [user][Interaction.user] who invoked the [Interaction] + * The [UserBehavior] of the [user][ActionInteraction.user] who invoked the [ActionInteraction] */ public val user: UserBehavior get() = UserBehavior(data.user, kord) @@ -52,6 +53,7 @@ public class MessageInteraction( */ public suspend fun getUser(): User = supplier.getUser(user.id) + /** * Requests to get the user of this interaction message, * returns null if the [User] isn't present. diff --git a/core/src/main/kotlin/event/interaction/ApplicationCreate.kt b/core/src/main/kotlin/event/interaction/ApplicationCreate.kt index e7fe9c383a2b..b7f638e84f8c 100644 --- a/core/src/main/kotlin/event/interaction/ApplicationCreate.kt +++ b/core/src/main/kotlin/event/interaction/ApplicationCreate.kt @@ -1,25 +1,44 @@ package dev.kord.core.event.interaction import dev.kord.core.Kord -import dev.kord.core.behavior.interaction.* -import dev.kord.core.entity.application.* -import dev.kord.core.entity.interaction.* +import dev.kord.core.behavior.interaction.AutoCompleteInteractionBehavior +import dev.kord.core.behavior.interaction.EphemeralInteractionResponseBehavior +import dev.kord.core.behavior.interaction.PublicInteractionResponseBehavior +import dev.kord.core.behavior.interaction.followUp +import dev.kord.core.behavior.interaction.respondEphemeral +import dev.kord.core.behavior.interaction.respondPublic +import dev.kord.core.entity.application.ApplicationCommand +import dev.kord.core.entity.interaction.ActionInteraction +import dev.kord.core.entity.interaction.ApplicationCommandInteraction +import dev.kord.core.entity.interaction.AutoCompleteInteraction +import dev.kord.core.entity.interaction.ChatInputCommandInvocationInteraction +import dev.kord.core.entity.interaction.GlobalApplicationCommandInteraction +import dev.kord.core.entity.interaction.GlobalAutoCompleteInteraction +import dev.kord.core.entity.interaction.GlobalChatInputCommandInteraction +import dev.kord.core.entity.interaction.GlobalMessageCommandInteraction +import dev.kord.core.entity.interaction.GlobalUserCommandInteraction +import dev.kord.core.entity.interaction.GuildApplicationCommandInteraction +import dev.kord.core.entity.interaction.GuildAutoCompleteInteraction +import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction +import dev.kord.core.entity.interaction.GuildMessageCommandInteraction +import dev.kord.core.entity.interaction.GuildUserCommandInteraction +import dev.kord.core.entity.interaction.MessageCommandInteraction +import dev.kord.core.entity.interaction.UserCommandInteraction import dev.kord.core.event.kordCoroutineScope import kotlinx.coroutines.CoroutineScope -import kotlin.coroutines.CoroutineContext /** * This event fires when an interaction is created. * * * Discord currently has one type of interaction, - * [Slash Commands][dev.kord.core.entity.interaction.ApplicationCommand]. + * [Slash Commands][ApplicationCommand]. * * The event should be acknowledged withing 3 seconds of reception using one of the following methods: - * * [acknowledgeEphemeral][Interaction.acknowledgeEphemeral] - acknowledges an interaction ephemerally. - * * [acknowledgePublic][Interaction.acknowledgePublic] - acknowledges an interaction in public. - * * [respondPublic][Interaction.respondPublic] - same as public acknowledgement, but an immediate result (message) can be supplied. - * * [respondEphemeral][Interaction.respondEphemeral] - same as ephemeral acknowledgement, but an immediate result (message) can be supplied. + * * [acknowledgeEphemeral][ActionInteraction.acknowledgeEphemeral] - acknowledges an interaction ephemerally. + * * [acknowledgePublic][ActionInteraction.acknowledgePublic] - acknowledges an interaction in public. + * * [respondPublic][ActionInteraction.respondPublic] - same as public acknowledgement, but an immediate result (message) can be supplied. + * * [respondEphemeral][ActionInteraction.respondEphemeral] - same as ephemeral acknowledgement, but an immediate result (message) can be supplied. * * Once an interaction has been acknowledged, * you can use [PublicInteractionResponseBehavior.followUp] or [EphemeralInteractionResponseBehavior.followUp] to display additional messages. @@ -46,7 +65,7 @@ public sealed interface GuildApplicationInteractionCreateEvent : ApplicationInte override val interaction: GuildApplicationCommandInteraction } -public sealed interface UserCommandInteractionCreateEvent : ApplicationInteractionCreateEvent { +public sealed interface UserCommandInteractionCreateEvent : ApplicationInteractionCreateEvent { override val interaction: UserCommandInteraction } @@ -65,7 +84,7 @@ public class GlobalUserCommandInteractionCreateEvent( ) : GlobalApplicationInteractionCreateEvent, UserCommandInteractionCreateEvent, CoroutineScope by coroutineScope -public sealed interface MessageCommandInteractionCreateEvent : ApplicationInteractionCreateEvent { +public sealed interface MessageCommandInteractionCreateEvent : ApplicationInteractionCreateEvent { override val interaction: MessageCommandInteraction } @@ -84,9 +103,8 @@ public class GlobalMessageCommandInteractionCreateEvent( ) : GlobalApplicationInteractionCreateEvent, MessageCommandInteractionCreateEvent, CoroutineScope by coroutineScope - -public sealed interface ChatInputCommandInteractionCreateEvent : ApplicationInteractionCreateEvent { - override val interaction: ChatInputCommandInteraction +public sealed interface ChatInputCommandInteractionCreateEvent : ApplicationInteractionCreateEvent { + override val interaction: ChatInputCommandInvocationInteraction } public class GuildChatInputCommandInteractionCreateEvent( @@ -102,3 +120,54 @@ public class GlobalChatInputCommandInteractionCreateEvent( override val shard: Int, public val coroutineScope: CoroutineScope = kordCoroutineScope(kord) ) : GlobalApplicationInteractionCreateEvent, ChatInputCommandInteractionCreateEvent, CoroutineScope by coroutineScope + +/** + * ActionInteraction received when a users types into an auto-completed option. + * + * Check [AutoCompleteInteractionBehavior] on how to reply. + * + * @see AutoCompleteInteraction + */ +public sealed interface AutoCompleteInteractionCreateEvent : InteractionCreateEvent + +internal fun AutoCompleteInteractionCreateEvent( + interaction: AutoCompleteInteraction, + kord: Kord, + shard: Int, + coroutineScope: CoroutineScope = kordCoroutineScope(kord) +): AutoCompleteInteractionCreateEvent = when (interaction) { + is GuildAutoCompleteInteraction -> GuildAutoCompleteInteractionCreateEvent( + kord, shard, interaction, coroutineScope + ) + else -> GlobalAutoCompleteInteractionCreateEvent( + kord, shard, interaction as GlobalAutoCompleteInteraction, coroutineScope + ) +} + +/** + * ActionInteraction received when a users types into an auto-completed option. + * + * Check [AutoCompleteInteractionBehavior] on how to reply. + * + * @see AutoCompleteInteraction + */ +public class GlobalAutoCompleteInteractionCreateEvent( + override val kord: Kord, + override val shard: Int, + override val interaction: GlobalAutoCompleteInteraction, + public val coroutineScope: CoroutineScope = kordCoroutineScope(kord) +) : AutoCompleteInteractionCreateEvent, CoroutineScope by coroutineScope + +/** + * ActionInteraction received when a users types into an auto-completed option. + * + * Check [AutoCompleteInteractionBehavior] on how to reply. + * + * @see AutoCompleteInteraction + */ +public class GuildAutoCompleteInteractionCreateEvent( + override val kord: Kord, + override val shard: Int, + override val interaction: GuildAutoCompleteInteraction, + public val coroutineScope: CoroutineScope = kordCoroutineScope(kord) +) : AutoCompleteInteractionCreateEvent, CoroutineScope by coroutineScope diff --git a/core/src/main/kotlin/exception/EntityNotFoundException.kt b/core/src/main/kotlin/exception/EntityNotFoundException.kt index 9ba3a7063965..bf29070924df 100644 --- a/core/src/main/kotlin/exception/EntityNotFoundException.kt +++ b/core/src/main/kotlin/exception/EntityNotFoundException.kt @@ -74,6 +74,6 @@ public class EntityNotFoundException : Exception { entityNotFound(T::class.simpleName!!, commandId) public inline fun interactionNotFound(token: String): Nothing = - throw EntityNotFoundException("Interaction with token $token was not found.") + throw EntityNotFoundException("ActionInteraction with token $token was not found.") } } diff --git a/core/src/main/kotlin/gateway/handler/InteractionEventHandler.kt b/core/src/main/kotlin/gateway/handler/InteractionEventHandler.kt index 2f3084e42437..c17ae05b7b5e 100644 --- a/core/src/main/kotlin/gateway/handler/InteractionEventHandler.kt +++ b/core/src/main/kotlin/gateway/handler/InteractionEventHandler.kt @@ -7,9 +7,48 @@ import dev.kord.core.Kord import dev.kord.core.cache.data.ApplicationCommandData import dev.kord.core.cache.data.InteractionData import dev.kord.core.cache.idEq -import dev.kord.core.entity.application.* -import dev.kord.core.entity.interaction.* -import dev.kord.core.event.interaction.* +import dev.kord.core.entity.application.GuildApplicationCommand +import dev.kord.core.entity.application.GuildChatInputCommand +import dev.kord.core.entity.application.GuildMessageCommand +import dev.kord.core.entity.application.GuildUserCommand +import dev.kord.core.entity.application.UnknownGuildApplicationCommand +import dev.kord.core.entity.interaction.AutoCompleteInteraction +import dev.kord.core.entity.interaction.GlobalButtonInteraction +import dev.kord.core.entity.interaction.GlobalChatInputCommandInteraction +import dev.kord.core.entity.interaction.GlobalMessageCommandInteraction +import dev.kord.core.entity.interaction.GlobalSelectMenuInteraction +import dev.kord.core.entity.interaction.GlobalUserCommandInteraction +import dev.kord.core.entity.interaction.GuildButtonInteraction +import dev.kord.core.entity.interaction.GuildChatInputCommandInteraction +import dev.kord.core.entity.interaction.GuildMessageCommandInteraction +import dev.kord.core.entity.interaction.GuildSelectMenuInteraction +import dev.kord.core.entity.interaction.GuildUserCommandInteraction +import dev.kord.core.entity.interaction.Interaction +import dev.kord.core.entity.interaction.UnknownApplicationCommandInteraction +import dev.kord.core.entity.interaction.UnknownComponentInteraction +import dev.kord.core.event.interaction.AutoCompleteInteractionCreateEvent +import dev.kord.core.event.interaction.ChatInputCommandCreateEvent +import dev.kord.core.event.interaction.ChatInputCommandDeleteEvent +import dev.kord.core.event.interaction.ChatInputCommandUpdateEvent +import dev.kord.core.event.interaction.GlobalButtonInteractionCreateEvent +import dev.kord.core.event.interaction.GlobalChatInputCommandInteractionCreateEvent +import dev.kord.core.event.interaction.GlobalMessageCommandInteractionCreateEvent +import dev.kord.core.event.interaction.GlobalSelectMenuInteractionCreateEvent +import dev.kord.core.event.interaction.GlobalUserCommandInteractionCreateEvent +import dev.kord.core.event.interaction.GuildButtonInteractionCreateEvent +import dev.kord.core.event.interaction.GuildChatInputCommandInteractionCreateEvent +import dev.kord.core.event.interaction.GuildMessageCommandInteractionCreateEvent +import dev.kord.core.event.interaction.GuildSelectMenuInteractionCreateEvent +import dev.kord.core.event.interaction.GuildUserCommandInteractionCreateEvent +import dev.kord.core.event.interaction.MessageCommandCreateEvent +import dev.kord.core.event.interaction.MessageCommandDeleteEvent +import dev.kord.core.event.interaction.MessageCommandUpdateEvent +import dev.kord.core.event.interaction.UnknownApplicationCommandCreateEvent +import dev.kord.core.event.interaction.UnknownApplicationCommandDeleteEvent +import dev.kord.core.event.interaction.UnknownApplicationCommandUpdateEvent +import dev.kord.core.event.interaction.UserCommandCreateEvent +import dev.kord.core.event.interaction.UserCommandDeleteEvent +import dev.kord.core.event.interaction.UserCommandUpdateEvent import dev.kord.gateway.* import kotlinx.coroutines.CoroutineScope import dev.kord.core.event.Event as CoreEvent @@ -30,8 +69,8 @@ public class InteractionEventHandler( private fun handle(event: InteractionCreate, shard: Int, kord: Kord, coroutineScope: CoroutineScope): CoreEvent { val data = InteractionData.from(event.interaction) - val interaction = Interaction.from(data, kord) - val coreEvent = when(interaction) { + val coreEvent = when(val interaction = Interaction.from(data, kord)) { + is AutoCompleteInteraction -> AutoCompleteInteractionCreateEvent(interaction, kord, shard, coroutineScope) is GlobalChatInputCommandInteraction -> GlobalChatInputCommandInteractionCreateEvent(interaction, kord, shard, coroutineScope) is GlobalUserCommandInteraction -> GlobalUserCommandInteractionCreateEvent(interaction, kord, shard, coroutineScope) is GlobalMessageCommandInteraction -> GlobalMessageCommandInteractionCreateEvent(interaction, kord, shard, coroutineScope) @@ -74,7 +113,6 @@ public class InteractionEventHandler( ): CoreEvent { val data = ApplicationCommandData.from(event.application) cache.put(data) - val application = GuildApplicationCommand(data, kord.rest.interaction) val coreEvent = when (val application = GuildApplicationCommand(data, kord.rest.interaction)) { is GuildChatInputCommand -> ChatInputCommandUpdateEvent(application, kord, shard, coroutineScope) diff --git a/rest/src/main/kotlin/builder/interaction/OptionsBuilder.kt b/rest/src/main/kotlin/builder/interaction/OptionsBuilder.kt index 77ce7f81b561..e41142fbb29c 100644 --- a/rest/src/main/kotlin/builder/interaction/OptionsBuilder.kt +++ b/rest/src/main/kotlin/builder/interaction/OptionsBuilder.kt @@ -1,7 +1,6 @@ package dev.kord.rest.builder.interaction import dev.kord.common.annotation.KordDsl -import dev.kord.common.annotation.KordPreview import dev.kord.common.entity.ApplicationCommandOption import dev.kord.common.entity.ApplicationCommandOptionType import dev.kord.common.entity.Choice @@ -27,12 +26,24 @@ sealed class OptionsBuilder( internal var _required: OptionalBoolean = OptionalBoolean.Missing var required: Boolean? by ::_required.delegate() + internal var _autocomplete: OptionalBoolean = OptionalBoolean.Missing + + /** + * Setting this to `true` allows you to dynamically respond with your choices, depending on the user input. + * + * This disables all input validation, users can submit values before responding to the AutoComplete request. + * + * Enabling this also means that you cannot add any other option. + */ + var autocomplete: Boolean? by ::_autocomplete.delegate() + override fun toRequest() = ApplicationCommandOption( type, name, description, _default, - _required + _required, + autocomplete = _autocomplete ) } @@ -41,8 +52,7 @@ sealed class BaseChoiceBuilder( name: String, description: String, type: ApplicationCommandOptionType -) : - OptionsBuilder(name, description, type) { +) : OptionsBuilder(name, description, type) { private var _choices: Optional>> = Optional.Missing() var choices: MutableList>? by ::_choices.delegate() @@ -54,7 +64,8 @@ sealed class BaseChoiceBuilder( description, choices = _choices, required = _required, - default = _default + default = _default, + autocomplete = _autocomplete ) } @@ -150,4 +161,4 @@ class GroupCommandBuilder(name: String, description: String) : if (options == null) options = mutableListOf() options!!.add(SubCommandBuilder(name, description).apply(builder)) } -} \ No newline at end of file +} diff --git a/rest/src/main/kotlin/json/request/InteractionsRequests.kt b/rest/src/main/kotlin/json/request/InteractionsRequests.kt index 38c7e816931b..aa7478d0e6bb 100644 --- a/rest/src/main/kotlin/json/request/InteractionsRequests.kt +++ b/rest/src/main/kotlin/json/request/InteractionsRequests.kt @@ -1,13 +1,19 @@ package dev.kord.rest.json.request -import dev.kord.common.annotation.KordPreview -import dev.kord.common.entity.* +import dev.kord.common.entity.AllowedMentions +import dev.kord.common.entity.ApplicationCommandOption +import dev.kord.common.entity.ApplicationCommandType +import dev.kord.common.entity.DiscordAttachment +import dev.kord.common.entity.DiscordAutoComplete +import dev.kord.common.entity.DiscordComponent +import dev.kord.common.entity.DiscordGuildApplicationCommandPermission +import dev.kord.common.entity.InteractionResponseType +import dev.kord.common.entity.MessageFlags import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.rest.NamedFile import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import java.io.InputStream @Serializable @@ -49,12 +55,17 @@ data class MultipartInteractionResponseModifyRequest( ) @Serializable - data class InteractionResponseCreateRequest( val type: InteractionResponseType, val data: Optional = Optional.Missing() ) +@Serializable +data class AutoCompleteResponseCreateRequest( + val type: InteractionResponseType, + val data: DiscordAutoComplete +) + data class MultipartInteractionResponseCreateRequest( val request: InteractionResponseCreateRequest, @@ -70,7 +81,7 @@ class InteractionApplicationCommandCallbackData( @SerialName("allowed_mentions") val allowedMentions: Optional = Optional.Missing(), val flags: Optional = Optional.Missing(), - val components: Optional> = Optional.Missing() + val components: Optional> = Optional.Missing(), ) @@ -114,5 +125,5 @@ data class MultipartFollowupMessageModifyRequest( @Serializable data class ApplicationCommandPermissionsEditRequest( - val permissions: List + val permissions: List ) diff --git a/rest/src/main/kotlin/service/InteractionService.kt b/rest/src/main/kotlin/service/InteractionService.kt index 678f3614bca4..93cbdaf203f3 100644 --- a/rest/src/main/kotlin/service/InteractionService.kt +++ b/rest/src/main/kotlin/service/InteractionService.kt @@ -3,16 +3,50 @@ package dev.kord.rest.service import dev.kord.common.entity.* import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.coerceToMissing +import dev.kord.common.entity.Choice +import dev.kord.common.entity.DiscordApplicationCommand +import dev.kord.common.entity.DiscordAutoComplete +import dev.kord.common.entity.DiscordGuildApplicationCommandPermissions +import dev.kord.common.entity.DiscordMessage +import dev.kord.common.entity.InteractionResponseType +import dev.kord.common.entity.PartialDiscordGuildApplicationCommandPermissions +import dev.kord.common.entity.Snowflake import dev.kord.common.entity.optional.orEmpty -import dev.kord.rest.builder.interaction.* +import dev.kord.rest.builder.interaction.ApplicationCommandPermissionsBulkModifyBuilder +import dev.kord.rest.builder.interaction.ApplicationCommandPermissionsModifyBuilder +import dev.kord.rest.builder.interaction.BaseChoiceBuilder +import dev.kord.rest.builder.interaction.ChatInputCreateBuilder +import dev.kord.rest.builder.interaction.ChatInputModifyBuilder +import dev.kord.rest.builder.interaction.IntChoiceBuilder +import dev.kord.rest.builder.interaction.MessageCommandCreateBuilder +import dev.kord.rest.builder.interaction.MessageCommandModifyBuilder +import dev.kord.rest.builder.interaction.MultiApplicationCommandBuilder +import dev.kord.rest.builder.interaction.NumberChoiceBuilder +import dev.kord.rest.builder.interaction.StringChoiceBuilder +import dev.kord.rest.builder.interaction.UserCommandCreateBuilder +import dev.kord.rest.builder.interaction.UserCommandModifyBuilder import dev.kord.rest.builder.message.create.FollowupMessageCreateBuilder import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder import dev.kord.rest.builder.message.modify.FollowupMessageModifyBuilder import dev.kord.rest.builder.message.modify.InteractionResponseModifyBuilder -import dev.kord.rest.json.request.* +import dev.kord.rest.json.request.ApplicationCommandCreateRequest +import dev.kord.rest.json.request.ApplicationCommandModifyRequest +import dev.kord.rest.json.request.ApplicationCommandPermissionsEditRequest +import dev.kord.rest.json.request.AutoCompleteResponseCreateRequest +import dev.kord.rest.json.request.FollowupMessageCreateRequest +import dev.kord.rest.json.request.FollowupMessageModifyRequest +import dev.kord.rest.json.request.InteractionApplicationCommandCallbackData +import dev.kord.rest.json.request.InteractionResponseCreateRequest +import dev.kord.rest.json.request.InteractionResponseModifyRequest +import dev.kord.rest.json.request.MultipartFollowupMessageCreateRequest +import dev.kord.rest.json.request.MultipartFollowupMessageModifyRequest +import dev.kord.rest.json.request.MultipartInteractionResponseCreateRequest +import dev.kord.rest.json.request.MultipartInteractionResponseModifyRequest import dev.kord.rest.request.RequestHandler import dev.kord.rest.route.Route +import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.serializer import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -124,6 +158,91 @@ class InteractionService(requestHandler: RequestHandler) : RestService(requestHa body(InteractionResponseCreateRequest.serializer(), request) } + suspend inline fun createAutoCompleteInteractionResponse( + interactionId: Snowflake, + interactionToken: String, + autoComplete: DiscordAutoComplete, + typeSerializer: KSerializer = serializer() + ) = call(Route.InteractionResponseCreate) { + keys[Route.InteractionId] = interactionId + keys[Route.InteractionToken] = interactionToken + + body( + AutoCompleteResponseCreateRequest.serializer(typeSerializer), + AutoCompleteResponseCreateRequest( + InteractionResponseType.ApplicationCommandAutoCompleteResult, + autoComplete + ) + ) + } + + @PublishedApi + internal suspend inline fun > createBuilderAutoCompleteInteractionResponse( + interactionId: Snowflake, + interactionToken: String, + builder: Builder, + builderFunction: Builder.() -> Unit + ) { + @Suppress("UNCHECKED_CAST") + val choices = builder.apply(builderFunction).choices as List> + + return createAutoCompleteInteractionResponse(interactionId, interactionToken, DiscordAutoComplete(choices)) + } + + @OptIn(ExperimentalContracts::class) + suspend inline fun createIntAutoCompleteInteractionResponse( + interactionId: Snowflake, + interactionToken: String, + builderFunction: IntChoiceBuilder.() -> Unit + ) { + contract { + callsInPlace(builderFunction, InvocationKind.EXACTLY_ONCE) + } + + return createBuilderAutoCompleteInteractionResponse( + interactionId, + interactionToken, + IntChoiceBuilder("", ""), + builderFunction + ) + } + + @OptIn(ExperimentalContracts::class) + suspend inline fun createNumberAutoCompleteInteractionResponse( + interactionId: Snowflake, + interactionToken: String, + builderFunction: NumberChoiceBuilder.() -> Unit + ) { + contract { + callsInPlace(builderFunction, InvocationKind.EXACTLY_ONCE) + } + + return createBuilderAutoCompleteInteractionResponse( + interactionId, + interactionToken, + NumberChoiceBuilder("", ""), + builderFunction + ) + } + + @OptIn(ExperimentalContracts::class) + suspend inline fun createStringAutoCompleteInteractionResponse( + interactionId: Snowflake, + interactionToken: String, + builderFunction: StringChoiceBuilder.() -> Unit + ) { + contract { + callsInPlace(builderFunction, InvocationKind.EXACTLY_ONCE) + } + + return createBuilderAutoCompleteInteractionResponse( + interactionId, + interactionToken, + StringChoiceBuilder("", ""), + builderFunction + ) + } + suspend fun getInteractionResponse( applicationId: Snowflake, interactionToken: String, @@ -367,7 +486,6 @@ class InteractionService(requestHandler: RequestHandler) : RestService(requestHa } - @OptIn(ExperimentalContracts::class) suspend inline fun createGuildChatInputApplicationCommand( applicationId: Snowflake, @@ -579,11 +697,11 @@ class InteractionService(requestHandler: RequestHandler) : RestService(requestHa } public suspend fun acknowledge(interactionId: Snowflake, interactionToken: String, ephemeral: Boolean = false) { - val request = InteractionResponseCreateRequest( + val request = InteractionResponseCreateRequest( type = InteractionResponseType.DeferredChannelMessageWithSource, data = Optional( InteractionApplicationCommandCallbackData( - flags = Optional(if(ephemeral) MessageFlags(MessageFlag.Ephemeral) else null).coerceToMissing() + flags = Optional(if (ephemeral) MessageFlags(MessageFlag.Ephemeral) else null).coerceToMissing() ) ) )