diff --git a/common/src/main/kotlin/entity/DiscordMessage.kt b/common/src/main/kotlin/entity/DiscordMessage.kt index 4cc1d91880a3..2821299b0b29 100644 --- a/common/src/main/kotlin/entity/DiscordMessage.kt +++ b/common/src/main/kotlin/entity/DiscordMessage.kt @@ -278,7 +278,9 @@ enum class MessageFlag(val code: Int) { SourceMessageDeleted(8), /* This message came from the urgent message system. */ - Urgent(16); + Urgent(16), + + Ephemeral(64); } @Serializable(with = MessageFlags.Serializer::class) diff --git a/common/src/main/kotlin/entity/Interactions.kt b/common/src/main/kotlin/entity/Interactions.kt index 8f114120b98e..7518a4a79598 100644 --- a/common/src/main/kotlin/entity/Interactions.kt +++ b/common/src/main/kotlin/entity/Interactions.kt @@ -18,27 +18,27 @@ val kordLogger = KotlinLogging.logger { } @Serializable @KordPreview data class DiscordApplicationCommand( - val id: Snowflake, - @SerialName("application_id") - val applicationId: Snowflake, - val name: String, - val description: String, - @SerialName("guild_id") - val guildId: OptionalSnowflake = OptionalSnowflake.Missing, - val options: Optional> = Optional.Missing(), + val id: Snowflake, + @SerialName("application_id") + val applicationId: Snowflake, + val name: String, + val description: String, + @SerialName("guild_id") + val guildId: OptionalSnowflake = OptionalSnowflake.Missing, + val options: Optional> = Optional.Missing(), ) @Serializable @KordPreview class ApplicationCommandOption( - val type: ApplicationCommandOptionType, - val name: String, - val description: String, - val default: OptionalBoolean = OptionalBoolean.Missing, - val required: OptionalBoolean = OptionalBoolean.Missing, - @OptIn(KordExperimental::class) - val choices: Optional>> = Optional.Missing(), - val options: Optional> = Optional.Missing(), + val type: ApplicationCommandOptionType, + val name: String, + val description: String, + val default: OptionalBoolean = OptionalBoolean.Missing, + val required: OptionalBoolean = OptionalBoolean.Missing, + @OptIn(KordExperimental::class) + val choices: Optional>> = Optional.Missing(), + val options: Optional> = Optional.Missing(), ) /** @@ -142,19 +142,29 @@ sealed class Choice { } } +@Serializable +@KordPreview +data class ResolvedObjects( + val members: Optional> = Optional.Missing(), + val users: Optional> = Optional.Missing(), + val roles: Optional> = Optional.Missing(), + val channels: Optional> = Optional.Missing() +) + @Serializable @KordPreview data class DiscordInteraction( - val id: Snowflake, - val type: InteractionType, - val data: DiscordApplicationCommandInteractionData, - @SerialName("guild_id") - val guildId: Snowflake, - @SerialName("channel_id") - val channelId: Snowflake, - val member: DiscordInteractionGuildMember, - val token: String, - val version: Int, + val id: Snowflake, + val type: InteractionType, + val data: DiscordApplicationCommandInteractionData, + @SerialName("guild_id") + val guildId: OptionalSnowflake = OptionalSnowflake.Missing, + @SerialName("channel_id") + val channelId: Snowflake, + val member: Optional = Optional.Missing(), + val user: Optional = Optional.Missing(), + val token: String, + val version: Int, ) @Serializable(InteractionType.Serializer::class) @@ -164,7 +174,7 @@ sealed class InteractionType(val type: Int) { object ApplicationCommand : InteractionType(2) class Unknown(type: Int) : InteractionType(type) - override fun toString(): String = when(this){ + override fun toString(): String = when (this) { Ping -> "InteractionType.Ping($type)" ApplicationCommand -> "InteractionType.ApplicationCommand($type)" is Unknown -> "InteractionType.Unknown($type)" @@ -194,9 +204,10 @@ sealed class InteractionType(val type: Int) { @Serializable @KordPreview data class DiscordApplicationCommandInteractionData( - val id: Snowflake, - val name: String, - val options: Optional> = Optional.Missing() + val id: Snowflake, + val name: String, + val resolved: Optional = Optional.Missing(), + val options: Optional> = Optional.Missing() ) @Serializable(with = Option.Serializer::class) @@ -233,7 +244,7 @@ sealed class Option { } jsonValue?.let { value -> // name + value == command option, i.e. an argument - return CommandArgument(name, OptionValue(value)) + return CommandArgument(name, DiscordOptionValue(value)) } if (jsonOptions == null) { // name -value -options == can only be sub command @@ -247,7 +258,8 @@ sealed class Option { return SubCommand(name, Optional(emptyList())) } - val onlyArguments = nestedOptions.all { it is CommandArgument } //only subcommand can have options at this point + val onlyArguments = + nestedOptions.all { it is CommandArgument } //only subcommand can have options at this point if (onlyArguments) return SubCommand(name, Optional(nestedOptions.filterIsInstance())) val onlySubCommands = nestedOptions.all { it is SubCommand } //only groups can have options at this point @@ -265,16 +277,16 @@ sealed class Option { @Serializable @KordPreview data class SubCommand( - override val name: String, - val options: Optional> = Optional.Missing() + override val name: String, + val options: Optional> = Optional.Missing() ) : Option() @Serializable @KordPreview data class CommandArgument( - override val name: String, - @OptIn(KordExperimental::class) - val value: OptionValue<@Serializable(NotSerializable::class) Any?>, + override val name: String, + @OptIn(KordExperimental::class) + val value: DiscordOptionValue<@Serializable(NotSerializable::class) Any?>, ) : Option() @Serializable @@ -284,21 +296,21 @@ data class CommandGroup( val options: Optional> = Optional.Missing(), ) : Option() -@Serializable(OptionValue.OptionValueSerializer::class) +@Serializable(DiscordOptionValue.OptionValueSerializer::class) @KordPreview -sealed class OptionValue(val value: T) { - class IntValue(value: Int) : OptionValue(value) - class StringValue(value: String) : OptionValue(value) - class BooleanValue(value: Boolean) : OptionValue(value) +sealed class DiscordOptionValue(val value: T) { + class IntValue(value: Int) : DiscordOptionValue(value) + class StringValue(value: String) : DiscordOptionValue(value) + class BooleanValue(value: Boolean) : DiscordOptionValue(value) - override fun toString(): String = when(this){ + override fun toString(): String = when (this) { is IntValue -> "OptionValue.IntValue($value)" is StringValue -> "OptionValue.StringValue($value)" is BooleanValue -> "OptionValue.BooleanValue($value)" } companion object { - operator fun invoke(value: JsonPrimitive): OptionValue = when { + operator fun invoke(value: JsonPrimitive): DiscordOptionValue = when { value.isString -> StringValue(value.content) value.booleanOrNull != null -> BooleanValue(value.boolean) value.intOrNull != null -> IntValue(value.int) @@ -306,10 +318,10 @@ sealed class OptionValue(val value: T) { } } - internal class OptionValueSerializer(serializer: KSerializer) : KSerializer> { + internal class OptionValueSerializer(serializer: KSerializer) : KSerializer> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("OptionValue", PrimitiveKind.STRING) - override fun deserialize(decoder: Decoder): OptionValue<*> { + override fun deserialize(decoder: Decoder): DiscordOptionValue<*> { val value = (decoder as JsonDecoder).decodeJsonElement().jsonPrimitive return when { value.isString -> StringValue(value.toString()) @@ -318,7 +330,7 @@ sealed class OptionValue(val value: T) { } } - override fun serialize(encoder: Encoder, value: OptionValue<*>) { + override fun serialize(encoder: Encoder, value: DiscordOptionValue<*>) { when (value) { is IntValue -> encoder.encodeInt(value.value) is StringValue -> encoder.encodeString(value.value) @@ -330,23 +342,23 @@ sealed class OptionValue(val value: T) { @KordPreview -fun OptionValue<*>.int(): Int { +fun DiscordOptionValue<*>.int(): Int { return value as? Int ?: error("$value wasn't an Int.") } @KordPreview -fun OptionValue<*>.string(): String { +fun DiscordOptionValue<*>.string(): String { return value.toString() } @KordPreview -fun OptionValue<*>.boolean(): Boolean { +fun DiscordOptionValue<*>.boolean(): Boolean { return value as? Boolean ?: error("$value wasn't a Boolean.") } @KordPreview -fun OptionValue<*>.snowflake(): Snowflake { +fun DiscordOptionValue<*>.snowflake(): Snowflake { val id = string().toLongOrNull() ?: error("$value wasn't a Snowflake") return Snowflake(id) } @@ -356,10 +368,8 @@ fun OptionValue<*>.snowflake(): Snowflake { @KordPreview sealed class InteractionResponseType(val type: Int) { object Pong : InteractionResponseType(1) - object Acknowledge : InteractionResponseType(2) - object ChannelMessage : InteractionResponseType(3) object ChannelMessageWithSource : InteractionResponseType(4) - object ACKWithSource : InteractionResponseType(5) + object DeferredChannelMessageWithSource : InteractionResponseType(5) class Unknown(type: Int) : InteractionResponseType(type) companion object; @@ -372,10 +382,8 @@ sealed class InteractionResponseType(val type: Int) { override fun deserialize(decoder: Decoder): InteractionResponseType { return when (val type = decoder.decodeInt()) { 1 -> Pong - 2 -> Acknowledge - 3 -> ChannelMessage 4 -> ChannelMessageWithSource - 5 -> ACKWithSource + 5 -> DeferredChannelMessageWithSource else -> Unknown(type) } } diff --git a/common/src/main/kotlin/entity/Member.kt b/common/src/main/kotlin/entity/Member.kt index 1ee931c40a70..e2fe982ac3a5 100644 --- a/common/src/main/kotlin/entity/Member.kt +++ b/common/src/main/kotlin/entity/Member.kt @@ -18,8 +18,8 @@ data class DiscordGuildMember( val joinedAt: String, @SerialName("premium_since") val premiumSince: Optional = Optional.Missing(), - val deaf: Boolean, - val mute: Boolean, + val deaf: OptionalBoolean = OptionalBoolean.Missing, + val mute: OptionalBoolean = OptionalBoolean.Missing, val pending: OptionalBoolean = OptionalBoolean.Missing ) @@ -37,8 +37,6 @@ data class DiscordInteractionGuildMember( val joinedAt: String, @SerialName("premium_since") val premiumSince: Optional = Optional.Missing(), - val deaf: Boolean, - val mute: Boolean, val permissions: Permissions, ) diff --git a/common/src/main/kotlin/entity/optional/Optional.kt b/common/src/main/kotlin/entity/optional/Optional.kt index 8a2ce6a260b0..c07789e33f34 100644 --- a/common/src/main/kotlin/entity/optional/Optional.kt +++ b/common/src/main/kotlin/entity/optional/Optional.kt @@ -213,6 +213,13 @@ inline fun Optional>.mapList(mapper: (E) -> T): Optional> } +@Suppress("UNCHECKED_CAST") +inline fun Optional>.mapValues(mapper: (Map.Entry) -> R): Optional> = when (this) { + is Missing, is Null<*> -> this as Optional> + is Value -> Value(value.mapValues(mapper)) +} + + @Suppress("UNCHECKED_CAST") inline fun Optional>.filterList(mapper: (E) -> Boolean): Optional> = when (this) { is Missing, is Null<*> -> this @@ -251,6 +258,16 @@ inline fun Optional.mapNotNull(mapper: (E) -> T): Optional = is Value -> Optional(mapper(value!!)) } +inline fun Optional>.firstOrNull(predicate: (E) -> Boolean) : E? = when(this){ + is Missing, is Null<*> -> null + is Value -> value.firstOrNull(predicate) +} + + +inline fun Optional>.first(predicate: (E) -> Boolean) : E = firstOrNull(predicate)!! + + + inline fun Optional.mapSnowflake(mapper: (E) -> Snowflake): OptionalSnowflake = when (this) { is Missing, is Null<*> -> OptionalSnowflake.Missing is Value -> OptionalSnowflake.Value(mapper(value)) diff --git a/common/src/test/kotlin/json/InteractionTest.kt b/common/src/test/kotlin/json/InteractionTest.kt index 4e3ce514fcf2..517920a3f781 100644 --- a/common/src/test/kotlin/json/InteractionTest.kt +++ b/common/src/test/kotlin/json/InteractionTest.kt @@ -1,9 +1,15 @@ package json import dev.kord.common.annotation.KordPreview -import dev.kord.common.entity.DiscordInteraction +import dev.kord.common.entity.* +import dev.kord.common.entity.optional.filterList +import dev.kord.common.entity.optional.orEmpty +import kotlinx.coroutines.coroutineScope import kotlinx.serialization.json.Json import org.junit.jupiter.api.Test +import kotlin.coroutines.suspendCoroutine +import kotlin.test.assertEquals +import kotlin.test.assertNotNull private fun file(name: String): String { val loader = InteractionTest::class.java.classLoader @@ -18,10 +24,75 @@ class InteractionTest { } @Test - fun `DiscordInteraction can be deserialized`() { - val text = file("interaction") + fun `group command can be deserialized`() { + val text = file("groupsubcommand") - json.decodeFromString(DiscordInteraction.serializer(), text) + val interaction = json.decodeFromString(DiscordInteraction.serializer(), text) + with(interaction) { + channelId shouldBe "587324906702766226" + id shouldBe "793442788670832640" + version shouldBe 1 + type.type shouldBe 2 + token shouldBe "hunter2" + data.name shouldBe "testsubcommands" + data.id shouldBe "792107855418490901" + val group = data.options.orEmpty().first() + assert(group is CommandGroup) + group as CommandGroup + group.name shouldBe "group" + val subCommand = group.options.orEmpty().first() + subCommand.name shouldBe "groupsubcommand" + val arg = subCommand.options.orEmpty().first() + arg.name shouldBe "testint" + arg.value.int() shouldBe 1 + arg.value.string() shouldBe "1" + } } + @Test + fun `subcommand can be deserialized`() { + val text = file("subcommand") + + val interaction = json.decodeFromString(DiscordInteraction.serializer(), text) + with(interaction) { + channelId shouldBe "587324906702766226" + id shouldBe "793442788670832640" + version shouldBe 1 + type.type shouldBe 2 + token shouldBe "hunter2" + data.name shouldBe "testsubcommands" + data.id shouldBe "792107855418490901" + val subCommand = data.options.orEmpty().first() + assert(subCommand is SubCommand) + subCommand as SubCommand + subCommand.name shouldBe "subcommand" + val arg = subCommand.options.orEmpty().first() + arg.name shouldBe "testint" + arg.value.int() shouldBe 1 + arg.value.string() shouldBe "1" + } + } + + + @Test + fun `root can be deserialized`() { + val text = file("rootcommand") + + val interaction = json.decodeFromString(DiscordInteraction.serializer(), text) + with(interaction) { + channelId shouldBe "587324906702766226" + id shouldBe "793442788670832640" + version shouldBe 1 + type.type shouldBe 2 + token shouldBe "hunter2" + data.name shouldBe "testsubcommands" + data.id shouldBe "792107855418490901" + val arg = data.options.orEmpty().first() + assert(arg is CommandArgument) + arg as CommandArgument + arg.name shouldBe "testint" + arg.value.int() shouldBe 1 + arg.value.string() shouldBe "1" + } + } } \ No newline at end of file diff --git a/common/src/test/resources/json/interaction/interaction.json b/common/src/test/resources/json/interaction/groupsubcommand.json similarity index 100% rename from common/src/test/resources/json/interaction/interaction.json rename to common/src/test/resources/json/interaction/groupsubcommand.json diff --git a/common/src/test/resources/json/interaction/rootcommand.json b/common/src/test/resources/json/interaction/rootcommand.json new file mode 100644 index 000000000000..9303a77c003c --- /dev/null +++ b/common/src/test/resources/json/interaction/rootcommand.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "type": 2, + "token": "hunter2", + "member": { + "user": { + "username": "Hope", + "public_flags": 64, + "id": "695549908383432716", + "discriminator": "9000", + "avatar": "68922a048551bb4b0dda88b65e85d8f1" + }, + "roles": [ + "556525712308305940" + ], + "premium_since": null, + "permissions": "2147483647", + "pending": false, + "nick": null, + "mute": false, + "joined_at": "2020-04-03T09:35:54.879000+00:00", + "is_pending": false, + "deaf": false + }, + "id": "793442788670832640", + "guild_id": "556525343595298817", + "data": { + "options": [ + { + "value": 1, + "name": "testint" + } + ], + "name": "testsubcommands", + "id": "792107855418490901" + }, + "channel_id": "587324906702766226" +} \ No newline at end of file diff --git a/common/src/test/resources/json/interaction/subcommand.json b/common/src/test/resources/json/interaction/subcommand.json new file mode 100644 index 000000000000..fd8cc71a8600 --- /dev/null +++ b/common/src/test/resources/json/interaction/subcommand.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "type": 2, + "token": "hunter2", + "member": { + "user": { + "username": "Hope", + "public_flags": 64, + "id": "695549908383432716", + "discriminator": "9000", + "avatar": "68922a048551bb4b0dda88b65e85d8f1" + }, + "roles": [ + "556525712308305940" + ], + "premium_since": null, + "permissions": "2147483647", + "pending": false, + "nick": null, + "mute": false, + "joined_at": "2020-04-03T09:35:54.879000+00:00", + "is_pending": false, + "deaf": false + }, + "id": "793442788670832640", + "guild_id": "556525343595298817", + "data": { + "options": [ + { + "options": [ + { + "value": 1, + "name": "testint" + } + ], + "name": "subcommand" + } + ], + "name": "testsubcommands", + "id": "792107855418490901" + }, + "channel_id": "587324906702766226" +} \ No newline at end of file diff --git a/core/src/main/kotlin/Kord.kt b/core/src/main/kotlin/Kord.kt index a8fd2e970779..e0186ea6e9e2 100644 --- a/core/src/main/kotlin/Kord.kt +++ b/core/src/main/kotlin/Kord.kt @@ -28,6 +28,7 @@ import dev.kord.gateway.Gateway import dev.kord.gateway.builder.PresenceBuilder import dev.kord.rest.builder.guild.GuildCreateBuilder import dev.kord.rest.builder.interaction.ApplicationCommandCreateBuilder +import dev.kord.rest.builder.interaction.ApplicationCommandsCreateBuilder import dev.kord.rest.builder.user.CurrentUserModifyBuilder import dev.kord.rest.service.RestClient import kotlinx.coroutines.* @@ -332,6 +333,13 @@ class Kord( description: String, builder: ApplicationCommandCreateBuilder.() -> Unit = {}, ) = slashCommands.createGlobalApplicationCommand(name, description, builder) + + @KordPreview + suspend inline fun createGlobalApplicationCommands( + builder: ApplicationCommandsCreateBuilder.() -> Unit, + ) = slashCommands.createGlobalApplicationCommands(builder) + + } /** diff --git a/core/src/main/kotlin/SlashCommands.kt b/core/src/main/kotlin/SlashCommands.kt index 20ec7b8ae259..6cb8a16c8afa 100644 --- a/core/src/main/kotlin/SlashCommands.kt +++ b/core/src/main/kotlin/SlashCommands.kt @@ -6,6 +6,7 @@ import dev.kord.core.cache.data.ApplicationCommandData import dev.kord.core.entity.interaction.GlobalApplicationCommand import dev.kord.core.entity.interaction.GuildApplicationCommand import dev.kord.rest.builder.interaction.ApplicationCommandCreateBuilder +import dev.kord.rest.builder.interaction.ApplicationCommandsCreateBuilder import dev.kord.rest.request.RequestHandler import dev.kord.rest.service.InteractionService import kotlinx.coroutines.flow.Flow @@ -36,6 +37,20 @@ class SlashCommands( return GlobalApplicationCommand(data, service) } + + @OptIn(ExperimentalContracts::class) + suspend inline fun createGlobalApplicationCommands( + builder: ApplicationCommandsCreateBuilder.() -> Unit, + ): List { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val request = ApplicationCommandsCreateBuilder().apply(builder).toRequest() + return service.createGlobalApplicationCommands(applicationId, request).map { + val data = ApplicationCommandData.from(it) + GlobalApplicationCommand(data, service) + } + + } + @OptIn(ExperimentalContracts::class) suspend inline fun createGuildApplicationCommand( guildId: Snowflake, @@ -50,6 +65,20 @@ class SlashCommands( return GuildApplicationCommand(data, service, guildId) } + + @OptIn(ExperimentalContracts::class) + suspend inline fun createGuildApplicationCommands( + guildId: Snowflake, + builder: ApplicationCommandsCreateBuilder.() -> Unit, + ): List { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val request = ApplicationCommandsCreateBuilder().apply(builder).toRequest() + return service.createGlobalApplicationCommands(applicationId, request).map { + val data = ApplicationCommandData.from(it) + GuildApplicationCommand(data, service, guildId) + } + } + fun getGuildApplicationCommands(guildId: Snowflake): Flow = flow { for (command in service.getGuildApplicationCommands(applicationId, guildId)) { val data = ApplicationCommandData.from(command) diff --git a/core/src/main/kotlin/behavior/GlobalApplicationCommandBehavior.kt b/core/src/main/kotlin/behavior/GlobalApplicationCommandBehavior.kt index 5eb56eaa805f..3c868ab523d3 100644 --- a/core/src/main/kotlin/behavior/GlobalApplicationCommandBehavior.kt +++ b/core/src/main/kotlin/behavior/GlobalApplicationCommandBehavior.kt @@ -58,7 +58,7 @@ interface GuildApplicationCommandBehavior : ApplicationCommandBehavior { override suspend fun edit(builder: suspend ApplicationCommandModifyBuilder.() -> Unit): GuildApplicationCommand { val request = ApplicationCommandModifyBuilder().apply { builder() }.toRequest() - val response = service.modifyGlobalApplicationCommand(applicationId, id, request) + val response = service.modifyGuildApplicationCommand(applicationId, guildId, id, request) val data = ApplicationCommandData.from(response) return GuildApplicationCommand(data, service, guildId) } diff --git a/core/src/main/kotlin/behavior/GuildBehavior.kt b/core/src/main/kotlin/behavior/GuildBehavior.kt index 1cfea3397976..3e19f47123e0 100644 --- a/core/src/main/kotlin/behavior/GuildBehavior.kt +++ b/core/src/main/kotlin/behavior/GuildBehavior.kt @@ -38,6 +38,7 @@ import dev.kord.rest.builder.guild.GuildModifyBuilder import dev.kord.rest.builder.guild.GuildWidgetModifyBuilder import dev.kord.rest.builder.guild.WelcomeScreenModifyBuilder import dev.kord.rest.builder.interaction.ApplicationCommandCreateBuilder +import dev.kord.rest.builder.interaction.ApplicationCommandsCreateBuilder import dev.kord.rest.builder.role.RoleCreateBuilder import dev.kord.rest.builder.role.RolePositionsModifyBuilder import dev.kord.rest.json.JsonErrorCode @@ -540,6 +541,12 @@ suspend inline fun GuildBehavior.createApplicationCommand( builder: ApplicationCommandCreateBuilder.() -> Unit = {}, ) = kord.slashCommands.createGuildApplicationCommand(id, name, description, builder) + +@KordPreview +suspend inline fun GuildBehavior.createApplicationCommands( + builder: ApplicationCommandsCreateBuilder.() -> Unit +) = kord.slashCommands.createGuildApplicationCommands(id, builder) + /** * Requests to edit this guild. * diff --git a/core/src/main/kotlin/behavior/GuildInteractionBehavior.kt b/core/src/main/kotlin/behavior/GuildInteractionBehavior.kt new file mode 100644 index 000000000000..0d8d980b6de8 --- /dev/null +++ b/core/src/main/kotlin/behavior/GuildInteractionBehavior.kt @@ -0,0 +1,18 @@ +package dev.kord.core.behavior + +import dev.kord.common.entity.Snowflake +import dev.kord.core.entity.Guild + +interface GuildInteractionBehavior : InteractionBehavior { + + val guildId: Snowflake + + suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId) + + suspend fun getGuild(): Guild = supplier.getGuild(guildId) + + companion object { + + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/behavior/InteractionBehavior.kt b/core/src/main/kotlin/behavior/InteractionBehavior.kt index 949bff5d860c..8136dac5e8cf 100644 --- a/core/src/main/kotlin/behavior/InteractionBehavior.kt +++ b/core/src/main/kotlin/behavior/InteractionBehavior.kt @@ -5,6 +5,7 @@ import dev.kord.common.entity.InteractionResponseType import dev.kord.common.entity.Snowflake import dev.kord.common.entity.optional.optional import dev.kord.core.Kord +import dev.kord.core.cache.data.InteractionData import dev.kord.core.entity.Guild import dev.kord.core.entity.KordEntity import dev.kord.core.entity.Strategizable @@ -22,7 +23,6 @@ interface InteractionBehavior : KordEntity, Strategizable { val applicationId: Snowflake val token: String - val guildId: Snowflake val channelId: Snowflake /** @@ -31,20 +31,12 @@ interface InteractionBehavior : KordEntity, Strategizable { * @param source weather to show the author's name and provided arguments of the command. * @return [InteractionResponseBehavior] which can be used to create follow-up message or edit the original response. */ - suspend fun acknowledge(source: Boolean = false): InteractionResponseBehavior { - val type = if (source) InteractionResponseType.ACKWithSource - else InteractionResponseType.Acknowledge - val request = InteractionResponseCreateRequest(type) + suspend fun acknowledge(): InteractionResponseBehavior { + val request = InteractionResponseCreateRequest(InteractionResponseType.DeferredChannelMessageWithSource) kord.rest.interaction.createInteractionResponse(id, token, request) return InteractionResponseBehavior(applicationId, token, kord) } - suspend fun getGuildOrNull(): Guild? = supplier.getGuildOrNull(guildId) - - - suspend fun getGuild(): Guild = supplier.getGuild(guildId) - - suspend fun getChannelOrNull(): Channel? = supplier.getChannelOrNull(channelId) @@ -52,13 +44,12 @@ interface InteractionBehavior : KordEntity, Strategizable { override fun withStrategy(strategy: EntitySupplyStrategy<*>): InteractionBehavior = - InteractionBehavior(id, guildId, channelId, token, applicationId, kord, strategy) + InteractionBehavior(id, channelId, token, applicationId, kord, strategy) companion object { operator fun invoke( id: Snowflake, - guildId: Snowflake, channelId: Snowflake, token: String, applicationId: Snowflake, @@ -81,8 +72,6 @@ interface InteractionBehavior : KordEntity, Strategizable { override val channelId: Snowflake get() = channelId - override val guildId: Snowflake - get() = guildId override val supplier: EntitySupplier get() = strategy.supply(kord) @@ -101,15 +90,12 @@ interface InteractionBehavior : KordEntity, Strategizable { @KordPreview @OptIn(ExperimentalContracts::class) suspend inline fun InteractionBehavior.respond( - source: Boolean = false, builder: InteractionApplicationCommandCallbackDataBuilder.() -> Unit = {} ): InteractionResponseBehavior { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val type = if (source) InteractionResponseType.ChannelMessageWithSource - else InteractionResponseType.ChannelMessage val data = InteractionApplicationCommandCallbackDataBuilder().apply(builder).build() - val request = InteractionResponseCreateRequest(type, data.optional()) + val request = InteractionResponseCreateRequest(InteractionResponseType.ChannelMessageWithSource, data.optional()) kord.rest.interaction.createInteractionResponse(id, token, request) return InteractionResponseBehavior(applicationId, token, kord) diff --git a/core/src/main/kotlin/cache/data/InteractionData.kt b/core/src/main/kotlin/cache/data/InteractionData.kt index 735fb403d0c2..6bdd6c0b92fd 100644 --- a/core/src/main/kotlin/cache/data/InteractionData.kt +++ b/core/src/main/kotlin/cache/data/InteractionData.kt @@ -4,8 +4,7 @@ import dev.kord.common.annotation.KordExperimental import dev.kord.common.annotation.KordPreview import dev.kord.common.entity.* import dev.kord.common.entity.NotSerializable -import dev.kord.common.entity.optional.Optional -import dev.kord.common.entity.optional.mapList +import dev.kord.common.entity.optional.* import dev.kord.gateway.InteractionCreate import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable @@ -19,11 +18,12 @@ data class InteractionData( val id: Snowflake, val type: InteractionType, val data: ApplicationCommandInteractionData, - val guildId: Snowflake, + val guildId: OptionalSnowflake = OptionalSnowflake.Missing, val channelId: Snowflake, - val member: MemberData, + val member: Optional = Optional.Missing(), + val user: Optional = Optional.Missing(), val token: String, - val permissions: Permissions, + val permissions: Optional, val version: Int ) { companion object { @@ -32,12 +32,13 @@ data class InteractionData( InteractionData( id, type, - ApplicationCommandInteractionData.from(data), + ApplicationCommandInteractionData.from(data, guildId.value), guildId, channelId, - member.toData(member.user.value!!.id,guildId), + member.map { it.toData(it.user.value!!.id, guildId.value!!) }, + user.map { it.toData() }, token, - member.permissions, + member.map { it.permissions }, version ) } @@ -45,20 +46,48 @@ data class InteractionData( } } +@KordPreview +@Serializable +data class ResolvedObjectsData( + val members: Optional> = Optional.Missing(), + val users: Optional> = Optional.Missing(), + val roles: Optional> = Optional.Missing(), + val channels: Optional> = Optional.Missing() +) { + companion object { + fun from(data: ResolvedObjects, guildId: Snowflake?): ResolvedObjectsData { + return ResolvedObjectsData( + members = data.members.mapValues { MemberData.from(it.key, guildId!!, it.value) }, + channels = data.channels.mapValues { ChannelData.from(it.value) }, + roles = data.roles.mapValues { RoleData.from(guildId!!, it.value) }, + users = data.users.mapValues { it.value.toData() } + ) + } + } +} + + @KordPreview @Serializable data class ApplicationCommandInteractionData( val id: Snowflake, val name: String, - val options: Optional> = Optional.Missing() + val options: Optional> = Optional.Missing(), + val resolvedObjectsData: Optional = Optional.Missing() ) { companion object { - fun from(data: DiscordApplicationCommandInteractionData): ApplicationCommandInteractionData { + fun from( + data: DiscordApplicationCommandInteractionData, + guildId: Snowflake? + ): ApplicationCommandInteractionData { return with(data) { ApplicationCommandInteractionData( id, name, - options.mapList { OptionData.from(it) }) + options.mapList { OptionData.from(it) }, + data.resolved.map { ResolvedObjectsData.from(it, guildId) } + ) + } } } @@ -67,15 +96,15 @@ data class ApplicationCommandInteractionData( @KordPreview @Serializable data class OptionData( - val name: String, - @OptIn(KordExperimental::class) - val value: Optional> = Optional.Missing(), - val values: Optional> = Optional.Missing(), - val subCommands: Optional> = Optional.Missing() + val name: String, + @OptIn(KordExperimental::class) + val value: Optional> = Optional.Missing(), + val values: Optional> = Optional.Missing(), + val subCommands: Optional> = Optional.Missing() ) { companion object { fun from(data: Option): OptionData = with(data) { - when(data) { + when (data) { is SubCommand -> OptionData(name, values = data.options) is CommandArgument -> OptionData(name, value = Optional(data.value)) is CommandGroup -> OptionData(name, subCommands = data.options) diff --git a/core/src/main/kotlin/entity/interaction/DmInteraction.kt b/core/src/main/kotlin/entity/interaction/DmInteraction.kt new file mode 100644 index 000000000000..b8e817e94ddc --- /dev/null +++ b/core/src/main/kotlin/entity/interaction/DmInteraction.kt @@ -0,0 +1,25 @@ +package dev.kord.core.entity.interaction + +import dev.kord.common.annotation.KordPreview +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.cache.data.InteractionData +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.DmChannel +import dev.kord.core.supplier.EntitySupplier + +/** + * An [Interaction] that took place in a [DmChannel]. + */ +@KordPreview +class DmInteraction( + override val data: InteractionData, + override val applicationId: Snowflake, + override val kord: Kord, + override val supplier: EntitySupplier = kord.defaultSupplier, +) : Interaction { + /** + * The user who invoked the interaction. + */ + val user get() = User(data.user.value!!, kord) +} \ No newline at end of file diff --git a/core/src/main/kotlin/entity/interaction/GuildInteraction.kt b/core/src/main/kotlin/entity/interaction/GuildInteraction.kt new file mode 100644 index 000000000000..bff7525ff828 --- /dev/null +++ b/core/src/main/kotlin/entity/interaction/GuildInteraction.kt @@ -0,0 +1,48 @@ +package dev.kord.core.entity.interaction + +import dev.kord.common.annotation.KordPreview +import dev.kord.common.entity.Permissions +import dev.kord.common.entity.Snowflake +import dev.kord.core.Kord +import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.behavior.GuildInteractionBehavior +import dev.kord.core.behavior.MemberBehavior +import dev.kord.core.behavior.channel.GuildMessageChannelBehavior +import dev.kord.core.cache.data.InteractionData +import dev.kord.core.entity.Guild +import dev.kord.core.supplier.EntitySupplier + +/** + * An [Interaction] that took place in a [Guild]. + */ +@KordPreview +class GuildInteraction( + override val data: InteractionData, + override val applicationId: Snowflake, + override val kord: Kord, + override val supplier: EntitySupplier +) : Interaction, GuildInteractionBehavior { + + override val guildId: Snowflake + get() = data.guildId.value!! + + /** + * Overridden permissions of the interaction invoker in the channel. + */ + val permissions: Permissions get() = data.permissions.value!! + + /** + * The [GuildBehavior] for the guild the command was executed in. + */ + val guild get() = GuildBehavior(guildId, kord) + + /** + * The invoker of the command as [MemberBehavior]. + */ + val member: MemberBehavior get() = MemberBehavior(guildId, data.member.value!!.userId, kord) + + override val channel: GuildMessageChannelBehavior + get() = GuildMessageChannelBehavior(guildId, channelId, kord) + + +} \ No newline at end of file diff --git a/core/src/main/kotlin/entity/interaction/Interaction.kt b/core/src/main/kotlin/entity/interaction/Interaction.kt index d54e07f910e2..69585549f128 100644 --- a/core/src/main/kotlin/entity/interaction/Interaction.kt +++ b/core/src/main/kotlin/entity/interaction/Interaction.kt @@ -1,132 +1,301 @@ package dev.kord.core.entity.interaction import dev.kord.common.annotation.KordPreview +import dev.kord.common.entity.DiscordOptionValue import dev.kord.common.entity.InteractionType -import dev.kord.common.entity.OptionValue -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.orEmpty +import dev.kord.common.entity.optional.* +import dev.kord.common.entity.snowflake import dev.kord.core.Kord -import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.KordObject import dev.kord.core.behavior.InteractionBehavior -import dev.kord.core.behavior.MemberBehavior -import dev.kord.core.behavior.channel.TextChannelBehavior +import dev.kord.core.behavior.channel.MessageChannelBehavior import dev.kord.core.cache.data.ApplicationCommandInteractionData import dev.kord.core.cache.data.InteractionData -import dev.kord.core.cache.data.OptionData -import dev.kord.core.entity.Entity -import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.cache.data.ResolvedObjectsData +import dev.kord.core.entity.Member +import dev.kord.core.entity.Role +import dev.kord.core.entity.User +import dev.kord.core.entity.channel.Channel +import dev.kord.core.supplier.EntitySupplyStrategy /** - * Interaction that can respond to interactions and follow them up. - * - * @property id interaction's id. - * @property channelId the channel id where the interaction took place. - * @property token a continuation token for responding to the interaction - * @property guildId the id of the guild where the interaction took place. - * @property permissions the permissions of the member with the overwrites. - * @property type the type of the interaction. - * @property member the invoker of the command as [MemberBehavior]. - * @property command [Command] object that contains the data related to the interaction's command. - * @property version read-only property, always 1 + * An instance of [Interaction] (https://discord.com/developers/docs/interactions/slash-commands#interaction) */ @KordPreview -class Interaction( - val data: InteractionData, - override val applicationId: Snowflake, - override val kord: Kord, - override val supplier: EntitySupplier = kord.defaultSupplier, -) : InteractionBehavior { +interface Interaction : InteractionBehavior { + + val data: InteractionData override val id: Snowflake get() = data.id + /** + * The channel id where the interaction took place. + */ override val channelId: Snowflake get() = data.channelId + /** + * a continuation token for responding to the interaction + */ override val token: String get() = data.token - override val guildId: Snowflake get() = data.guildId - + /** + * The type of the interaction. + */ val type: InteractionType get() = data.type - val permissions: Permissions get() = data.permissions - - val channel: TextChannelBehavior get() = TextChannelBehavior(id = channelId, guildId = guildId, kord = kord) + /** + * The [MessageChannelBehavior] of the channel the command was executed in. + */ + val channel: MessageChannelBehavior get() = MessageChannelBehavior(data.channelId, kord) - val guild get() = GuildBehavior(guildId, kord) - val member: MemberBehavior get() = MemberBehavior(data.guildId, data.member.userId, kord) - - val command: Command - get() = Command(data.data) + /** + * [InteractionCommand] object that contains the data related to the interaction's command. + */ + val command: InteractionCommand + get() = InteractionCommand(data.data, kord) + /** + * read-only property, always 1 + */ val version: Int get() = data.version + companion object { + fun from( + data: InteractionData, + kord: Kord, + strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy + ): Interaction { + return if (data.guildId !is OptionalSnowflake.Missing) + GuildInteraction(data, kord.slashCommands.applicationId, kord, strategy.supply(kord)) + else + DmInteraction(data, kord.slashCommands.applicationId, kord, strategy.supply(kord)) + } + } + } /** - * The root command in the interaction. - * - * @property name name of the command. - * @property options names of options in the command mapped to their values. - * @property groups names of groups in the command mapped to the [Group] with matching name. - * @property subCommands names of sub-commands in the command mapped to the [SubCommand] with matching name. + * The base command of all commands that can be executed under an interaction event. */ @KordPreview -class Command(val data: ApplicationCommandInteractionData) : Entity { - override val id: Snowflake - get() = data.id +sealed class InteractionCommand : KordObject { + /** + * The id of the root command. + */ + abstract val rootId: Snowflake - val name get() = data.name + /** + * The root command name + */ + abstract val rootName: String - val options - get(): Map> = data.options.orEmpty() - .filter { it.value !is Optional.Missing<*> } - .associate { it.name to it.value.value!! } + /** + * the values passed to the command. + */ + abstract val options: Map> - val groups: Map - get() = data.options.orEmpty() - .filter { it.subCommands.orEmpty().isNotEmpty() } - .associate { it.name to Group(it) } + companion object { + operator fun invoke( + data: ApplicationCommandInteractionData, + 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 application command") + } + } + } + + abstract val resolved: ResolvedObjects? +} - val subCommands: Map +/** + * Represents an invocation of a root command. + * + * The root command is the first command defined in in a slash-command structure. + */ +@KordPreview +class RootCommand( + val data: ApplicationCommandInteractionData, + override val kord: Kord +) : InteractionCommand() { + + override val rootId: Snowflake + get() = data.id + + override val rootName get() = data.name + + override val options: Map> get() = data.options.orEmpty() - .filter { it.values.orEmpty().isNotEmpty() } - .associate { it.name to SubCommand(it) } + .associate { it.name to OptionValue(it.value.value!!, resolved) } + override val resolved: ResolvedObjects? + get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } } /** - * The Group containing [SubCommand]s related to [Command]. - * - *@property subCommands names of sub-commands in this [Group] of commands mapped to the [SubCommand] with matching name. + * Represents an invocation of a sub-command under the [RootCommand] */ @KordPreview -class Group(val data: OptionData) { - val name: String get() = data.name +class SubCommand( + val data: ApplicationCommandInteractionData, + override val kord: Kord +) : InteractionCommand() { + + private val subCommandData = data.options.orEmpty().first() + + override val rootName get() = data.name + + override val rootId: Snowflake + get() = data.id + + /** + * Name of the sub-command executed. + */ + val name get() = subCommandData.name + + override val options: Map> + get() = subCommandData.values.orEmpty() + .associate { it.name to OptionValue(it.value, resolved) } + + + override val resolved: ResolvedObjects? + get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } - val subCommands: Map - get() = data.subCommands.orEmpty() - .associate { it.name to SubCommand(OptionData(it.name, values = it.options)) } } /** - * A [SubCommand] that is either a part of [Command] or [Group]. - * - * @property name name of the subcommand. - * @property options names of options in the command mapped to their values. + * Represents an invocation of a sub-command under a group. */ @KordPreview -class SubCommand(val data: OptionData) { - val name: String get() = data.name +class GroupCommand( + val data: ApplicationCommandInteractionData, + override val kord: Kord +) : InteractionCommand() { + + private val groupData get() = data.options.orEmpty().first() + private val subCommandData get() = groupData.subCommands.orEmpty().first() - val options: Map> - get() = data.values.orEmpty() - .associate { it.name to it.value } + override val rootId: Snowflake + get() = data.id + + override val rootName get() = data.name + + /** + * Name of the group of this sub-command. + */ + val groupName get() = groupData.name + + /** + * Name of this sub-command + */ + val name get() = subCommandData.name + + override val options: Map> + get() = subCommandData.options.orEmpty() + .associate { it.name to OptionValue(it.value, resolved) } + + + override val resolved: ResolvedObjects? + get() = data.resolvedObjectsData.unwrap { ResolvedObjects(it, kord) } + +} + +@KordPreview +class ResolvedObjects( + val data: ResolvedObjectsData, + val kord: Kord, + val strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy +) { + val channels: Map? + get() = data.channels.mapValues { + Channel.from( + it.value, + kord, + strategy + ) + }.value + val roles: Map? get() = data.roles.mapValues { Role(it.value, kord) }.value + val users: Map? get() = data.users.mapValues { User(it.value, kord) }.value + val members: Map? + get() = data.members.mapValues { + Member( + it.value, + users!!.get(it.key)!!.data, + kord + ) + }.value + +} + +@KordPreview +sealed class OptionValue(val value: T) { + + class RoleOptionValue(value: Role) : OptionValue(value) + open class UserOptionValue(value: User) : OptionValue(value) + class MemberOptionValue(value: Member) : UserOptionValue(value) + class ChannelOptionValue(value: Channel) : OptionValue(value) + class IntOptionValue(value: Int) : OptionValue(value) + class StringOptionValue(value: String) : OptionValue(value) + class BooleanOptionValue(value: Boolean) : OptionValue(value) + + companion object { + operator fun invoke(value: DiscordOptionValue<*>, resolvedObjects: ResolvedObjects?): OptionValue<*> { + return when (value) { + is DiscordOptionValue.BooleanValue -> BooleanOptionValue(value.value) + is DiscordOptionValue.IntValue -> IntOptionValue(value.value) + is DiscordOptionValue.StringValue -> { + if (resolvedObjects == null) return StringOptionValue(value.value) + + val snowflake = value.snowflake() + + when { + resolvedObjects.members?.get(snowflake) != null -> + MemberOptionValue(resolvedObjects.members?.get(snowflake)!!) + resolvedObjects.users?.get(snowflake) != null -> + UserOptionValue(resolvedObjects.users?.get(snowflake)!!) + resolvedObjects.channels?.get(snowflake) != null -> + ChannelOptionValue(resolvedObjects.channels?.get(snowflake)!!) + resolvedObjects.roles?.get(snowflake) != null -> + RoleOptionValue(resolvedObjects.roles?.get(snowflake)!!) + else -> StringOptionValue(value.value) + } + } + } + } + } } +@KordPreview +fun OptionValue<*>.user(): User = value as User + +@KordPreview +fun OptionValue<*>.channel(): Channel = value as Channel +@KordPreview +fun OptionValue<*>.role(): Role = value as Role +@KordPreview +fun OptionValue<*>.member(): Member = value as Member + +@KordPreview +fun OptionValue<*>.string() = value.toString() + +@KordPreview +fun OptionValue<*>.boolean() = value as Boolean + +@KordPreview +fun OptionValue<*>.int() = value as Int diff --git a/core/src/main/kotlin/gateway/handler/InteractionEventHandler.kt b/core/src/main/kotlin/gateway/handler/InteractionEventHandler.kt index 92e30acbace9..3fd63373871c 100644 --- a/core/src/main/kotlin/gateway/handler/InteractionEventHandler.kt +++ b/core/src/main/kotlin/gateway/handler/InteractionEventHandler.kt @@ -33,7 +33,7 @@ class InteractionEventHandler( private suspend fun handle(event: InteractionCreate, shard: Int) { val data = InteractionData.from(event) - val interaction = Interaction(data, kord.selfId, kord) + val interaction = Interaction.from(data, kord) coreFlow.emit(InteractionCreateEvent(interaction, kord, shard)) } diff --git a/core/src/test/kotlin/interaction/CommandTypesTest.kt b/core/src/test/kotlin/interaction/CommandTypesTest.kt new file mode 100644 index 000000000000..b9af24e79d80 --- /dev/null +++ b/core/src/test/kotlin/interaction/CommandTypesTest.kt @@ -0,0 +1,82 @@ +package interaction + +import dev.kord.common.annotation.KordPreview +import dev.kord.common.entity.DiscordApplicationCommandInteractionData +import dev.kord.core.cache.data.ApplicationCommandInteractionData +import dev.kord.core.entity.interaction.* +import kotlinx.serialization.json.* +import mockKord +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +@KordPreview +class CommandsTypeTests { + val arg = buildJsonObject { + put("name", "argument") + put("value", 1) + } + val root = buildJsonObject { + put("id", "792107855418490901") + put("name", "root") + putJsonArray("options") { add(arg) } + } + val partialSubCommand = buildJsonObject { + put("name", "subCommand") + putJsonArray("options") { add(arg) } + } + val subCommand = + buildJsonObject { + putJsonArray("options") { add(partialSubCommand) } + put("name", "root") + put("id", "792107855418490901") + } + + val group = buildJsonObject { + putJsonArray("options") { + addJsonObject { + put("name", "group") + putJsonArray("options") { add(partialSubCommand) } + } + } + put("name", "root") + put("id", "792107855418490901") + } + + @Test + fun `Correctly infer RootCommand`() { + val serializedRoot = Json.decodeFromJsonElement(DiscordApplicationCommandInteractionData.serializer(), root) + val data = ApplicationCommandInteractionData.from(serializedRoot, null) + val command = InteractionCommand(data, mockKord()) + assert(command is RootCommand) + command as RootCommand + assertEquals(1, command.options["argument"]?.int()) + assertEquals("root", command.rootName) + + } + + @Test + fun `Correctly infer subcommand`() { + val sub = Json.decodeFromJsonElement(DiscordApplicationCommandInteractionData.serializer(), subCommand) + val data = ApplicationCommandInteractionData.from(sub, null) + val command = InteractionCommand(data, mockKord()) + assert(command is SubCommand) + command as SubCommand + assertEquals(1, command.options["argument"]?.int()) + assertEquals("root", command.rootName) + assertEquals("subCommand", command.name) + + } + + @Test + fun `Correctly infer group`() { + val grouping = Json.decodeFromJsonElement(DiscordApplicationCommandInteractionData.serializer(), group) + val data = ApplicationCommandInteractionData.from(grouping, null) + val command = InteractionCommand(data, mockKord()) + assert(command is GroupCommand) + command as GroupCommand + assertEquals(1, command.options["argument"]?.int()) + assertEquals("root", command.rootName) + assertEquals("group", command.groupName) + assertEquals("subCommand", command.name) + } +} \ No newline at end of file diff --git a/core/src/test/resources/interaction/groupsubcommand.json b/core/src/test/resources/interaction/groupsubcommand.json new file mode 100644 index 000000000000..bff896333550 --- /dev/null +++ b/core/src/test/resources/interaction/groupsubcommand.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "type": 2, + "token": "hunter2", + "member": { + "user": { + "username": "Hope", + "public_flags": 64, + "id": "695549908383432716", + "discriminator": "9000", + "avatar": "68922a048551bb4b0dda88b65e85d8f1" + }, + "roles": [ + "556525712308305940" + ], + "premium_since": null, + "permissions": "2147483647", + "pending": false, + "nick": null, + "mute": false, + "joined_at": "2020-04-03T09:35:54.879000+00:00", + "is_pending": false, + "deaf": false + }, + "id": "793442788670832640", + "guild_id": "556525343595298817", + "data": { + "options": [ + { + "options": [ + { + "options": [ + { + "value": 1, + "name": "testint" + } + ], + "name": "groupsubcommand" + } + ], + "name": "group" + } + ], + "name": "testsubcommands", + "id": "792107855418490901" + }, + "channel_id": "587324906702766226" +} \ No newline at end of file diff --git a/core/src/test/resources/interaction/rootcommand.json b/core/src/test/resources/interaction/rootcommand.json new file mode 100644 index 000000000000..9303a77c003c --- /dev/null +++ b/core/src/test/resources/interaction/rootcommand.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "type": 2, + "token": "hunter2", + "member": { + "user": { + "username": "Hope", + "public_flags": 64, + "id": "695549908383432716", + "discriminator": "9000", + "avatar": "68922a048551bb4b0dda88b65e85d8f1" + }, + "roles": [ + "556525712308305940" + ], + "premium_since": null, + "permissions": "2147483647", + "pending": false, + "nick": null, + "mute": false, + "joined_at": "2020-04-03T09:35:54.879000+00:00", + "is_pending": false, + "deaf": false + }, + "id": "793442788670832640", + "guild_id": "556525343595298817", + "data": { + "options": [ + { + "value": 1, + "name": "testint" + } + ], + "name": "testsubcommands", + "id": "792107855418490901" + }, + "channel_id": "587324906702766226" +} \ No newline at end of file diff --git a/core/src/test/resources/interaction/subcommand.json b/core/src/test/resources/interaction/subcommand.json new file mode 100644 index 000000000000..fd8cc71a8600 --- /dev/null +++ b/core/src/test/resources/interaction/subcommand.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "type": 2, + "token": "hunter2", + "member": { + "user": { + "username": "Hope", + "public_flags": 64, + "id": "695549908383432716", + "discriminator": "9000", + "avatar": "68922a048551bb4b0dda88b65e85d8f1" + }, + "roles": [ + "556525712308305940" + ], + "premium_since": null, + "permissions": "2147483647", + "pending": false, + "nick": null, + "mute": false, + "joined_at": "2020-04-03T09:35:54.879000+00:00", + "is_pending": false, + "deaf": false + }, + "id": "793442788670832640", + "guild_id": "556525343595298817", + "data": { + "options": [ + { + "options": [ + { + "value": 1, + "name": "testint" + } + ], + "name": "subcommand" + } + ], + "name": "testsubcommands", + "id": "792107855418490901" + }, + "channel_id": "587324906702766226" +} \ No newline at end of file diff --git a/rest/src/main/kotlin/builder/interaction/InteractionsBuilder.kt b/rest/src/main/kotlin/builder/interaction/InteractionsBuilder.kt index dffd7b773dac..3fd4c70d4ef7 100644 --- a/rest/src/main/kotlin/builder/interaction/InteractionsBuilder.kt +++ b/rest/src/main/kotlin/builder/interaction/InteractionsBuilder.kt @@ -40,6 +40,24 @@ class ApplicationCommandCreateBuilder( } +@KordPreview +@KordDsl +class ApplicationCommandsCreateBuilder : RequestBuilder> { + val commands: MutableList = mutableListOf() + fun command( + name: String, + description: String, + builder: ApplicationCommandCreateBuilder.() -> Unit + ) { + commands += ApplicationCommandCreateBuilder(name, description).apply(builder) + } + + override fun toRequest(): List { + return commands.map { it.toRequest() } + } + +} + @KordPreview @KordDsl sealed class BaseApplicationBuilder { @@ -270,12 +288,12 @@ class FollowupMessageCreateBuilder : RequestBuilder( DiscordApplicationCommand.serializer() ) + + object GlobalApplicationCommandsCreate : Route>( + HttpMethod.Put, + "/applications/${ApplicationId}/commands", + ListSerializer(DiscordApplicationCommand.serializer()) + ) + object GlobalApplicationCommandModify : Route( HttpMethod.Patch, "/applications/${ApplicationId}/commands/${CommandId}", @@ -486,6 +493,13 @@ sealed class Route( DiscordApplicationCommand.serializer() ) + + object GuildApplicationCommandsCreate : Route>( + HttpMethod.Put, + "/applications/${ApplicationId}/guilds/${GuildId}/commands", + ListSerializer(DiscordApplicationCommand.serializer()) + ) + object GuildApplicationCommandModify : Route( HttpMethod.Patch, diff --git a/rest/src/main/kotlin/service/InteractionService.kt b/rest/src/main/kotlin/service/InteractionService.kt index ca1032b5ce2c..8b94beed8303 100644 --- a/rest/src/main/kotlin/service/InteractionService.kt +++ b/rest/src/main/kotlin/service/InteractionService.kt @@ -6,6 +6,8 @@ import dev.kord.common.entity.Snowflake import dev.kord.rest.json.request.* import dev.kord.rest.request.RequestHandler import dev.kord.rest.route.Route +import kotlinx.serialization.builtins.ListSerializer + @KordPreview class InteractionService(requestHandler: RequestHandler) : RestService(requestHandler) { suspend fun getGlobalApplicationCommands(applicationId: Snowflake): List = @@ -21,6 +23,15 @@ class InteractionService(requestHandler: RequestHandler) : RestService(requestHa body(ApplicationCommandCreateRequest.serializer(), request) } + + suspend fun createGlobalApplicationCommands( + applicationId: Snowflake, + request: List + ): List = call(Route.GlobalApplicationCommandsCreate) { + keys[Route.ApplicationId] = applicationId + body(ListSerializer(ApplicationCommandCreateRequest.serializer()), request) + } + suspend fun modifyGlobalApplicationCommand( applicationId: Snowflake, commandId: Snowflake, @@ -47,13 +58,23 @@ class InteractionService(requestHandler: RequestHandler) : RestService(requestHa applicationId: Snowflake, guildId: Snowflake, request: ApplicationCommandCreateRequest - ) = - call(Route.GuildApplicationCommandCreate) { + ) = call(Route.GuildApplicationCommandCreate) { keys[Route.ApplicationId] = applicationId keys[Route.GuildId] = guildId body(ApplicationCommandCreateRequest.serializer(), request) } + suspend fun createGuildApplicationCommands( + applicationId: Snowflake, + guildId: Snowflake, + request: List + ) = call(Route.GuildApplicationCommandsCreate) { + keys[Route.ApplicationId] = applicationId + keys[Route.GuildId] = guildId + body(ListSerializer(ApplicationCommandCreateRequest.serializer()), request) + } + + suspend fun modifyGuildApplicationCommand( applicationId: Snowflake, guildId: Snowflake,