From dab0d77190217b07fb46c6d4418c7471305d3709 Mon Sep 17 00:00:00 2001 From: Bart Arys Date: Sat, 12 Jun 2021 17:34:19 +0200 Subject: [PATCH] Improve slash command API and add support for components (#310) * Make slash command creation eager createGuildApplicationCommands and createGlobalApplicationCommands should eagerly create new commands to be more in line with the rest of the API * Fix typo in InteractionBehavior ackowledgePublic -> acknowledgePublic * Don't compute supplier in InteractionBehavior It's a waste of resources, and might result in unexpected behavior for non-Kord suppliers. * Specify withStrategy for Interactions Return types are important! * Introduce type to command options Also add Mentionable. Seems like Discord consistently tells us what kind of option an object is via the type field, this allows us to more clearly represent the json data in the lower level APIs. I ended up deleting DiscordOptionValue and replacing it with a more fleshed out CommandArgument. * Add KordDsl to builders * Add allowedMentions builder functions * Add permission edits to guild commands * Make full member available for guild contexts * Support buttons/components (#303) * Add new API models and properties * Add new InteractionCallbackType * Add ability to send components * Update core entities * Update tests * Add @KordPreview annotations * Convert don't trust doc comments to normal comments * Fix compilation issue * Optimize interaction serializing * Add builder * Fix tests * Apply some requested changes * Hopefully fix formatting * Make builders enforce Discord restrictions * Improve builders again * Remove LinkButtonBuilder.style * Improve builders - Add components to more supported builders - Make required fields parameters - Make builders appending * Update deprecated message (#280) * Expose the creation of application commands behavior (#281) * Fix GuildUpdate core handling (#284) * Expose the creation of application commands behavior * Fix type of emitted event * Sealed message types (#282) * Expose the creation of application commands behavior * Make message types sealed * make Unknown a class * Add missing message types * make MessageTypeSerializer internal * Add buttons to Activity (#287) * Add buttons to Activity * Also pass buttons in constructor * Add missing fields to Guild (#288) * Add missing fields to Guild - Add welcome_screen - Add nsfw * Fix failing tests * Fix another failing tests * Add Message.applicationId (#289) * Message interaction (#283) * Expose the creation of application commands behavior * Add interaction message * Apply suggestions * reference the MessageInteraction in docs * Implement Strategizable for MessageInteraction * cache user from interaction message * Fix compilation errors * Fix withStrategy return type Co-authored-by: Bart Arys Co-authored-by: Bart Arys * Implement Stage instances (#291) * Add low-level implementation of stage instances * Add helper functions * Add core entities and api representations * Expose creation of StageInstanceBehavior to unsafe - Revert outdated change * Final additions - Add StageInstanceBehavior.asStageInstance - Fix compiler issue - Add StageChannelBehavior.getStageInstance() * Add StageInstances to EntitySupplier.kt * Add StageInstances to EntitySupplier.kt * Fix typo * Apply requested changes * Fix return type of channel in VoiceState (#295) * Fix return type of channel in VoiceState * Fix GuildService#modifyVoiceState endpoint * Fix MessageInteraction#user id * Add missing session start limit field (#306) - Adds the max_concurrency field to the session start limit data class * Update to Kotlin 1.5 (#299) * Update to 1.5.0-RC * trigger on branch pushes * Port to Kotlin 1.5 (#268) * Port dependencies to Kotlin 1.5 - Convert AbstractRateLimiter.AbstractRequestToken to a static rather than an inner class due to a compiler bug - Downgrade kx.ser-json to 1.0.0 to avoid a compiler bug - Bump other Kotlin dependencies to latest fixup! Port dependencies to Kotlin 1.5 - Convert AbstractRateLimiter.AbstractRequestToken to a static rather than an inner class due to a compiler bug - Downgrade kx.ser-json to 1.0.0 to avoid a compiler bug - Bump other Kotlin dependencies to latest * Replace deprecated kotlin.time APIs * Replace more deprecated APIs & inline classes * Replace deprecated usage of time API in tests * Possibly fix test Co-authored-by: Hope <34831095+HopeBaron@users.noreply.github.com> * Port to Kotlin 1.5 - Bump dependencies to 1.5 recommended versions - Remove inline classes in favor of value classes - Add required opt-ins - Migrate some more deprecated apis * Fix some gradle issues * Fix Gradle compilation issue * Remove documentationFileName as the new dokka version doesn't support it anymore and there is no replacement yet * Port kotlinx.serialization to 1.2.0 (#279) * Port kotlinx.serialization to 1.2.0 - Convert local classes to top level classes (See Kotlin/kotlinx.serialization#1472) - Improve handling of empty JSON bodies (See Kotlin/kotlinx.serialization#678) - Fix Failing Command test * Fix failing test * Properly decode null in gateway events (#286) * Properly decode null in gateway events * Update gateway/src/main/kotlin/Event.kt Co-authored-by: Bart Arys Co-authored-by: Bart Arys * Update Kotlinx.serialization (#290) * Update Kotlin 1.5 branch to upstream (#292) * Implement voice stage channel (#239) * compute All * implement rest endpoints * JSON representation * implement core representation * handle stage channels * Apply suggestions Co-authored-by: BartArys * Remove duplicated factory function Co-authored-by: BartArys * add documentation * Document the requestToSpeak variable Co-authored-by: BartArys * Fix CI triggers * Add "Competing" activity type (Fix #270) (#272) * Make Updatestatus activities not-null (#274) As per Discord's documentation: https://github.com/discord/discord-api-docs/pull/2789 * Fix memory issues related to Permission combining (#277) * Do not octuple bitset size on copy the pure plus and minus function create a new array to work with, this incorrectly created an array of a size equal to the amount of bits that were allocated, instead the amount of longs. Thus, octupling the internal size. * Optimize Permission All The All Permission folded each DiscordBitSet of each value into eachother, resulting in n + 1 bitsets being created. This commit changes that to use the internal `add` which instead, which only mutates the single bitset created. * Add Stream permission It was missing * Add Permission All regression tests * Update deprecated message (#280) * Expose the creation of application commands behavior (#281) * Fix GuildUpdate core handling (#284) * Expose the creation of application commands behavior * Fix type of emitted event * Sealed message types (#282) * Expose the creation of application commands behavior * Make message types sealed * make Unknown a class * Add missing message types * make MessageTypeSerializer internal * Add buttons to Activity (#287) * Add buttons to Activity * Also pass buttons in constructor * Add missing fields to Guild (#288) * Add missing fields to Guild - Add welcome_screen - Add nsfw * Fix failing tests * Fix another failing tests * Add Message.applicationId (#289) Co-authored-by: Hope <34831095+HopeBaron@users.noreply.github.com> Co-authored-by: BartArys Co-authored-by: HopeBaron Co-authored-by: Bart Arys Co-authored-by: Noah Hendrickson * Fix broken CI (#293) * Migrate to kotlinx-datetime (#297) * Migrate API code to kotlinx-datetime * Update tests * Remove dead code * Replace iso serializing with kx.dt * Bump dependencies to Kotlin 1.5.10 (#305) * Bump dependencies to Kotlin 1.5.10 - Fix sample code * Update remaining dependencies * Update ktor to 1.6.0 Co-authored-by: HopeBaron Co-authored-by: Michael Rittmeister Co-authored-by: Hope <34831095+HopeBaron@users.noreply.github.com> Co-authored-by: Noah Hendrickson * Filter out non-guilds when fetching rest guilds (#301) This is a side-effect from the system we currently employ when it comes to creating channels. Unknown channels are instantiated as an anonymous channel without hierarchy. While we could assume it is a guild channel, it's not in actuality. So the solution, as we did with cache, is to filter out channels that don't inherit the hierarchy. * Improve builders again - Merge in upstream * Fix builder requirements - Fix error when sending a emoji button * Fix merge related syntax errors * Restore default sample * Fix formatting isssue in documentation Co-authored-by: Bart Arys * Max component list a val Co-authored-by: Bart Arys * Apply suggestions from code review Co-authored-by: Bart Arys * Apply requested changes * No longer make DiscordInteraction a sealed class * Fix DiscordInteraction serialization * Add some documentation for components * Remove unused imports from builder Co-authored-by: Noah Hendrickson Co-authored-by: Hope <34831095+HopeBaron@users.noreply.github.com> Co-authored-by: Bart Arys Co-authored-by: 2D Co-authored-by: HopeBaron * Add core versions of components * Restructure and document ButtonBuilder * Remove ActionRowContainerBuilder * Make ComponentInteraction message nullable ephemeral messages in the interaction contain a id, which is not a message id, and the flags. We can't construct a behaviour from this (since the id isn't real), so I decided to drop the data if the message is ephemeral. * Add missing components to interaction builders * Add missing ComponentInteraction behavior * Fix withStrategy for ComponentInteractionBehavior * Implement ComponentInteractionBehavior * Move component builders directory We use singular for package names * Fix interaction embeds optionality * Make CommandInteraction#guildId optional * Make MessageModifyBuilder components vals Co-authored-by: Michael Rittmeister Co-authored-by: Noah Hendrickson Co-authored-by: Hope <34831095+HopeBaron@users.noreply.github.com> Co-authored-by: 2D Co-authored-by: HopeBaron --- .../src/main/kotlin/entity/DiscordMessage.kt | 6 + core/src/main/kotlin/Unsafe.kt | 17 +- .../main/kotlin/cache/data/ComponentData.kt | 6 +- .../src/main/kotlin/cache/data/MessageData.kt | 11 +- core/src/main/kotlin/entity/Message.kt | 11 +- .../kotlin/entity/interaction/Interaction.kt | 31 ++- .../entity/interaction/MessageInteraction.kt | 2 +- .../EphemeralInteractionBuilders.kt | 32 ++- .../interaction/FollowupMessageBuilders.kt | 238 +++++++++--------- .../builder/interaction/OptionsBuilder.kt | 1 + .../interaction/PublicInteractionBuilder.kt | 60 ++++- .../builder/message/MessageModifyBuilder.kt | 13 +- .../json/request/InteractionsRequests.kt | 13 +- 13 files changed, 272 insertions(+), 169 deletions(-) diff --git a/common/src/main/kotlin/entity/DiscordMessage.kt b/common/src/main/kotlin/entity/DiscordMessage.kt index 9fc5d7f3a2fa..dfaca1529502 100644 --- a/common/src/main/kotlin/entity/DiscordMessage.kt +++ b/common/src/main/kotlin/entity/DiscordMessage.kt @@ -63,6 +63,7 @@ import kotlin.contracts.contract * @param stickers The stickers sent with the message (bots currently can only receive messages with stickers, not send). * @param referencedMessage the message associated with [messageReference]. * @param applicationId if the message is a response to an [Interaction][DiscordInteraction], this is the id of the interaction's application + * @param components a list of [components][DiscordComponent] which have been added to this message */ @Serializable data class DiscordMessage( @@ -103,6 +104,11 @@ data class DiscordMessage( val stickers: Optional> = Optional.Missing(), @SerialName("referenced_message") val referencedMessage: Optional = Optional.Missing(), + /* + * don't trust the docs: + * This is a list even though the docs say it's a component + */ + val components: Optional> = Optional.Missing(), val interaction: Optional = Optional.Missing() ) diff --git a/core/src/main/kotlin/Unsafe.kt b/core/src/main/kotlin/Unsafe.kt index c949920656d5..4db516cc811b 100644 --- a/core/src/main/kotlin/Unsafe.kt +++ b/core/src/main/kotlin/Unsafe.kt @@ -2,10 +2,10 @@ package dev.kord.core import dev.kord.common.annotation.KordExperimental import dev.kord.common.annotation.KordUnsafe - import dev.kord.common.entity.Snowflake import dev.kord.core.behavior.* import dev.kord.core.behavior.channel.* +import dev.kord.core.behavior.interaction.ComponentInteractionBehavior import dev.kord.rest.service.InteractionService /** @@ -91,4 +91,17 @@ class Unsafe(private val kord: Kord) { ): GlobalApplicationCommandBehavior = GlobalApplicationCommandBehavior(applicationId, commandId, service) -} \ No newline at end of file + /** + * Creates a ComponentInteractionBehavior with the given [id], [channelId], + * [token] and [applicationId]. + */ + fun componentInteraction( + id: Snowflake, + channelId: Snowflake, + token: String, + applicationId: Snowflake = kord.selfId, + ): ComponentInteractionBehavior = ComponentInteractionBehavior( + id, channelId, token, applicationId, kord + ) + +} diff --git a/core/src/main/kotlin/cache/data/ComponentData.kt b/core/src/main/kotlin/cache/data/ComponentData.kt index db68257d90e6..8d53f3c391e4 100644 --- a/core/src/main/kotlin/cache/data/ComponentData.kt +++ b/core/src/main/kotlin/cache/data/ComponentData.kt @@ -6,7 +6,6 @@ import dev.kord.common.entity.DiscordComponent import dev.kord.common.entity.DiscordPartialEmoji import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.OptionalBoolean -import dev.kord.common.entity.optional.mapList import kotlinx.serialization.Serializable @Serializable @@ -24,7 +23,7 @@ data class ComponentData( companion object { - fun from(entity: DiscordComponent): ComponentData = with(entity) { + fun from(entity: DiscordComponent) = with(entity) { ComponentData( type, style, @@ -32,8 +31,7 @@ data class ComponentData( emoji, customId, url, - disabled, - components.mapList { from(it) } + disabled ) } diff --git a/core/src/main/kotlin/cache/data/MessageData.kt b/core/src/main/kotlin/cache/data/MessageData.kt index c85ff39b8289..0607a90a7eb8 100644 --- a/core/src/main/kotlin/cache/data/MessageData.kt +++ b/core/src/main/kotlin/cache/data/MessageData.kt @@ -36,7 +36,8 @@ data class MessageData( val flags: Optional = Optional.Missing(), val stickers: Optional> = Optional.Missing(), val referencedMessage: Optional = Optional.Missing(), - val interaction: Optional = Optional.Missing() + val interaction: Optional = Optional.Missing(), + val components: Optional> = Optional.Missing() ) { fun plus(selfId: Snowflake, reaction: MessageReactionAddData): MessageData { @@ -103,7 +104,8 @@ data class MessageData( flags, stickers = stickers, referencedMessage = referencedMessage, - interaction = interaction + interaction = interaction, + components = components ) } @@ -138,10 +140,11 @@ data class MessageData( flags, stickers.mapList { MessageStickerData.from(it) }, referencedMessage.mapNotNull { from(it) }, - interaction.map { MessageInteractionData.from(it) } + interaction.map { MessageInteractionData.from(it) }, + components = components.mapList { ComponentData.from(it) } ) } } } -fun DiscordMessage.toData() = MessageData.from(this) \ No newline at end of file +fun DiscordMessage.toData() = MessageData.from(this) diff --git a/core/src/main/kotlin/entity/Message.kt b/core/src/main/kotlin/entity/Message.kt index 96bd7debbfdc..cadb15a09e1c 100644 --- a/core/src/main/kotlin/entity/Message.kt +++ b/core/src/main/kotlin/entity/Message.kt @@ -3,9 +3,9 @@ package dev.kord.core.entity import dev.kord.common.annotation.KordPreview import dev.kord.common.entity.MessageType import dev.kord.common.entity.Snowflake -import dev.kord.common.entity.optional.map import dev.kord.common.entity.optional.mapNullable import dev.kord.common.entity.optional.orEmpty +import dev.kord.common.entity.optional.unwrap import dev.kord.common.exception.RequestException import dev.kord.core.Kord import dev.kord.core.behavior.MessageBehavior @@ -16,8 +16,9 @@ import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.GuildChannel import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.entity.channel.MessageChannel -import dev.kord.core.entity.interaction.MessageInteraction +import dev.kord.core.entity.component.Component import dev.kord.core.entity.interaction.Interaction +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 @@ -108,7 +109,7 @@ class Message( /** * If the message is a response to an [Interaction], this is the id of the interaction's application */ - val applicationId: Snowflake? get() = data.applicationId.value + val applicationId: Snowflake? get() = data.application.unwrap { it.id } /** * The message being replied to. @@ -232,6 +233,10 @@ class Message( */ val webhookId: Snowflake? get() = data.webhookId.value + @KordPreview + val components: List + get() = data.components.orEmpty().map { Component(it) } + /** * Returns itself. */ diff --git a/core/src/main/kotlin/entity/interaction/Interaction.kt b/core/src/main/kotlin/entity/interaction/Interaction.kt index 096d1784ddb3..a399fa2acf30 100644 --- a/core/src/main/kotlin/entity/interaction/Interaction.kt +++ b/core/src/main/kotlin/entity/interaction/Interaction.kt @@ -1,10 +1,7 @@ package dev.kord.core.entity.interaction import dev.kord.common.annotation.KordPreview -import dev.kord.common.entity.CommandArgument -import dev.kord.common.entity.InteractionType -import dev.kord.common.entity.Permissions -import dev.kord.common.entity.Snowflake +import dev.kord.common.entity.* import dev.kord.common.entity.optional.* import dev.kord.core.Kord import dev.kord.core.KordObject @@ -14,18 +11,25 @@ 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.ComponentInteractionBehavior +import dev.kord.core.behavior.interaction.EphemeralInteractionResponseBehavior import dev.kord.core.behavior.interaction.InteractionBehavior +import dev.kord.core.behavior.interaction.PublicInteractionResponseBehavior 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.channel.DmChannel import dev.kord.core.entity.channel.ResolvedChannel -import dev.kord.core.entity.component.ActionRowComponent import dev.kord.core.entity.component.ButtonComponent import dev.kord.core.entity.component.Component import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kord.rest.builder.interaction.UpdateMessageInteractionResponseCreateBuilder +import dev.kord.rest.json.request.InteractionApplicationCommandCallbackData +import dev.kord.rest.json.request.InteractionResponseCreateRequest +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract /** * An instance of [Interaction] (https://discord.com/developers/docs/interactions/slash-commands#interaction) @@ -303,11 +307,11 @@ fun OptionValue(value: CommandArgument<*>, resolvedObjects: ResolvedObjects?): O is CommandArgument.MentionableArgument -> { val channel = resolvedObjects?.channels.orEmpty()[value.value] - val user = resolvedObjects?.users.orEmpty()[value.value] + val user = resolvedObjects?.channels.orEmpty()[value.value] val member = resolvedObjects?.members.orEmpty()[value.value] - val role = resolvedObjects?.roles.orEmpty()[value.value] + val role = resolvedObjects?.members.orEmpty()[value.value] - OptionValue.MentionableOptionValue((channel ?: member ?: user ?: role)!!) + OptionValue.MentionableOptionValue((channel ?: user ?: member ?: role)!!) } is CommandArgument.RoleArgument -> { @@ -381,11 +385,9 @@ class ComponentInteraction( * * @see Component */ - val component: ButtonComponent? + val component: ButtonComponent get() = message?.components.orEmpty() - .filterIsInstance() - .flatMap { it.buttons } - .firstOrNull { it.customId == componentId } + .filterIsInstance().first { it.customId == componentId } override fun withStrategy(strategy: EntitySupplyStrategy<*>): ComponentInteraction = ComponentInteraction( @@ -459,8 +461,3 @@ fun OptionValue<*>.boolean() = value as Boolean @KordPreview fun OptionValue<*>.int() = value as Int - -@KordPreview -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 280614b10a65..dbc82e78480a 100644 --- a/core/src/main/kotlin/entity/interaction/MessageInteraction.kt +++ b/core/src/main/kotlin/entity/interaction/MessageInteraction.kt @@ -31,7 +31,7 @@ class MessageInteraction( override val id: Snowflake get() = data.id /** - * the [name][ApplicationCommand.name] of the [ApplicationCommand] that triggered this message. + * the [name][ApplicationCommand.name] of the [ApplicationCommand] that triggered this message. */ val name: String get() = data.name diff --git a/rest/src/main/kotlin/builder/interaction/EphemeralInteractionBuilders.kt b/rest/src/main/kotlin/builder/interaction/EphemeralInteractionBuilders.kt index d131c56964ae..beee94c55848 100644 --- a/rest/src/main/kotlin/builder/interaction/EphemeralInteractionBuilders.kt +++ b/rest/src/main/kotlin/builder/interaction/EphemeralInteractionBuilders.kt @@ -1,5 +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.InteractionResponseType import dev.kord.common.entity.MessageFlag @@ -7,8 +8,9 @@ import dev.kord.common.entity.MessageFlags import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.delegate.delegate import dev.kord.common.entity.optional.map -import dev.kord.common.entity.optional.mapList import dev.kord.common.entity.optional.optional +import dev.kord.rest.builder.component.ActionRowBuilder +import dev.kord.rest.builder.component.MessageComponentBuilder import dev.kord.rest.builder.message.AllowedMentionsBuilder import dev.kord.rest.builder.message.EmbedBuilder import dev.kord.rest.json.request.* @@ -16,6 +18,7 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract +@KordDsl @KordPreview class EphemeralInteractionResponseModifyBuilder : BaseInteractionResponseModifyBuilder { private var _content: Optional = Optional.Missing() @@ -23,23 +26,40 @@ class EphemeralInteractionResponseModifyBuilder : BaseInteractionResponseModifyB override val embeds: MutableList = mutableListOf() - private var _allowedMentions: Optional = Optional.Missing() override var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() + val components: MutableList = mutableListOf() + + @OptIn(ExperimentalContracts::class) + inline fun allowedMentions(builder: AllowedMentionsBuilder.() -> Unit) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(builder) + } + + @OptIn(ExperimentalContracts::class) + @KordPreview + inline fun actionRow(builder: ActionRowBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + components.add(ActionRowBuilder().apply(builder)) + } + override fun toRequest(): MultipartInteractionResponseModifyRequest { return MultipartInteractionResponseModifyRequest( InteractionResponseModifyRequest( content = _content, allowedMentions = _allowedMentions.map { it.build() }, + components = Optional.missingOnEmpty(components.map { it.build() }), embeds = embeds.map { it.toRequest() } ) ) - } } - +@KordDsl @KordPreview class EphemeralInteractionResponseCreateBuilder : BaseInteractionResponseCreateBuilder { private var _content: Optional = Optional.Missing() @@ -61,11 +81,11 @@ class EphemeralInteractionResponseCreateBuilder : BaseInteractionResponseCreateB val data = InteractionApplicationCommandCallbackData( content = _content, flags = flags, - embeds = embeds.map { it.toRequest() } + embeds = Optional.missingOnEmpty(embeds.map { it.toRequest() }) ) return MultipartInteractionResponseCreateRequest( InteractionResponseCreateRequest(type, data.optional()) ) } -} \ No newline at end of file +} diff --git a/rest/src/main/kotlin/builder/interaction/FollowupMessageBuilders.kt b/rest/src/main/kotlin/builder/interaction/FollowupMessageBuilders.kt index 5f7adfdba108..4f6676b20e30 100644 --- a/rest/src/main/kotlin/builder/interaction/FollowupMessageBuilders.kt +++ b/rest/src/main/kotlin/builder/interaction/FollowupMessageBuilders.kt @@ -12,6 +12,7 @@ import dev.kord.rest.builder.component.ActionRowBuilder import dev.kord.rest.builder.component.MessageComponentBuilder import dev.kord.rest.builder.message.AllowedMentionsBuilder import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.json.request.EmbedRequest import dev.kord.rest.json.request.FollowupMessageCreateRequest import dev.kord.rest.json.request.FollowupMessageModifyRequest import dev.kord.rest.json.request.MultipartFollowupMessageCreateRequest @@ -26,103 +27,41 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract @KordPreview -sealed interface FollowupMessageBuilder : RequestBuilder { - - var allowedMentions: AllowedMentionsBuilder? - - val embeds: MutableList? - - val components: MutableList? +@KordDsl +class PublicFollowupMessageModifyBuilder : + RequestBuilder { + private var _content: Optional = Optional.Missing() + var content: String? by ::_content.delegate() - val content: String? + private var _embeds: Optional> = Optional.Missing() + var embeds: MutableList? by ::_embeds.delegate() -} -@KordPreview -@OptIn(ExperimentalContracts::class) -inline fun FollowupMessageBuilder.actionRow(builder: ActionRowBuilder.() -> Unit) { - contract { - callsInPlace(builder, InvocationKind.EXACTLY_ONCE) - } + val files: MutableList> = mutableListOf() - when (this) { - is EphemeralFollowupMessageCreateBuilder -> { - components.add(ActionRowBuilder().apply(builder)) - } - is EphemeralFollowupMessageModifyBuilder -> { - components = (components ?: mutableListOf()).also { - it.add(ActionRowBuilder().apply(builder)) - } - } + private var _allowedMentions: Optional = Optional.Missing() + var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() - is PublicFollowupMessageCreateBuilder -> { - components.add(ActionRowBuilder().apply(builder)) - } + @KordPreview + val components: MutableList = mutableListOf() - is PublicFollowupMessageModifyBuilder -> { - components = (components ?: mutableListOf()).also { - it.add(ActionRowBuilder().apply(builder)) - } - } + @OptIn(ExperimentalContracts::class) + inline fun embed(builder: EmbedBuilder.() -> Unit) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + if (embeds == null) embeds = mutableListOf() + embeds!! += EmbedBuilder().apply(builder) } -} -@KordPreview -@OptIn(ExperimentalContracts::class) -inline fun FollowupMessageBuilder.embed(builder: EmbedBuilder.() -> Unit) { - contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - - when(this){ - is EphemeralFollowupMessageCreateBuilder -> { - embeds.add(EmbedBuilder().apply(builder)) - } - is EphemeralFollowupMessageModifyBuilder -> { - embeds = (embeds ?: mutableListOf()).also { - it.add(EmbedBuilder().apply(builder)) - } - } - is PublicFollowupMessageCreateBuilder -> { - embeds.add(EmbedBuilder().apply(builder)) + @OptIn(ExperimentalContracts::class) + @KordPreview + inline fun actionRow(builder: ActionRowBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - is PublicFollowupMessageModifyBuilder -> { - embeds = (embeds ?: mutableListOf()).also { - it.add(EmbedBuilder().apply(builder)) - } - } - } -} - -/** - * Configures the mentions that should trigger a mention (aka ping). Not calling this function will result in the default behavior - * (ping everything), calling this function but not configuring it before the request is build will result in all - * pings being ignored. - */ -@KordPreview -@OptIn(ExperimentalContracts::class) -inline fun FollowupMessageBuilder.allowedMentions(block: AllowedMentionsBuilder.() -> Unit = {}) { - contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(block) -} - - - -@KordPreview -@KordDsl -class PublicFollowupMessageModifyBuilder : - FollowupMessageBuilder { - private var _content: Optional = Optional.Missing() - override var content: String? by ::_content.delegate() - - private var _embeds: Optional> = Optional.Missing() - override var embeds: MutableList? by ::_embeds.delegate() - val files: MutableList> = mutableListOf() - - private var _allowedMentions: Optional = Optional.Missing() - override var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() + components.add(ActionRowBuilder().apply(builder)) + } - private var _components: Optional> = Optional.Missing() - override var components: MutableList? by ::_components.delegate() fun addFile(name: String, content: InputStream) { files += name to content @@ -132,13 +71,25 @@ class PublicFollowupMessageModifyBuilder : addFile(path.fileName.toString(), Files.newInputStream(path)) } + /** + * Configures the mentions that should trigger a mention (aka ping). Not calling this function will result in the default behavior + * (ping everything), calling this function but not configuring it before the request is build will result in all + * pings being ignored. + */ + @OptIn(ExperimentalContracts::class) + inline fun allowedMentions(block: AllowedMentionsBuilder.() -> Unit = {}) { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(block) + } + + override fun toRequest(): MultipartFollowupMessageModifyRequest { return MultipartFollowupMessageModifyRequest( FollowupMessageModifyRequest( _content, _embeds.mapList { it.toRequest() }, _allowedMentions.map { it.build() }, - _components.mapList { it.build() }, + Optional.missingOnEmpty(components.map(MessageComponentBuilder::build)) ), files ) @@ -149,25 +100,35 @@ class PublicFollowupMessageModifyBuilder : @KordPreview @KordDsl class EphemeralFollowupMessageModifyBuilder : - FollowupMessageBuilder { - private var _content: Optional = Optional.Missing() - override var content: String? by ::_content.delegate() + RequestBuilder { + private var _content: Optional = Optional.Missing() + var content: String? by ::_content.delegate() private var _embeds: Optional> = Optional.Missing() - override var embeds: MutableList? by ::_embeds.delegate() + var embeds: MutableList? by ::_embeds.delegate() private var _allowedMentions: Optional = Optional.Missing() - override var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() + var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() + + val components: MutableList = mutableListOf() + - private var _components: Optional> = Optional.Missing() - override var components: MutableList? by ::_components.delegate() + /** + * Configures the mentions that should trigger a mention (aka ping). Not calling this function will result in the default behavior + * (ping everything), calling this function but not configuring it before the request is build will result in all + * pings being ignored. + */ + @OptIn(ExperimentalContracts::class) + inline fun allowedMentions(block: AllowedMentionsBuilder.() -> Unit = {}) { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(block) + } override fun toRequest(): FollowupMessageModifyRequest { return FollowupMessageModifyRequest( content = _content, allowedMentions = _allowedMentions.map { it.build() }, - embeds = _embeds.mapList { it.toRequest() }, - components = _components.mapList { it.build() } + components = Optional.missingOnEmpty(components.map(MessageComponentBuilder::build)) ) } } @@ -175,29 +136,66 @@ class EphemeralFollowupMessageModifyBuilder : @KordPreview @KordDsl -class PublicFollowupMessageCreateBuilder : FollowupMessageBuilder { +class PublicFollowupMessageCreateBuilder : RequestBuilder { private var _content: Optional = Optional.Missing() - override var content: String? by ::_content.delegate() + var content: String? by ::_content.delegate() + private var _tts: OptionalBoolean = OptionalBoolean.Missing var tts: Boolean? by ::_tts.delegate() private var _allowedMentions: Optional = Optional.Missing() - override var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() + var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() val files: MutableList> = mutableListOf() + var embeds: MutableList = mutableListOf() - override val embeds: MutableList = mutableListOf() + val components: MutableList = mutableListOf() - override val components: MutableList = mutableListOf() + fun addFile(name: String, content: InputStream) { + files += name to content + } + + suspend fun addFile(path: Path) = withContext(Dispatchers.IO) { + addFile(path.fileName.toString(), Files.newInputStream(path)) + } + + /** + * Configures the mentions that should trigger a mention (aka ping). Not calling this function will result in the default behavior + * (ping everything), calling this function but not configuring it before the request is build will result in all + * pings being ignored. + */ + @OptIn(ExperimentalContracts::class) + inline fun allowedMentions(block: AllowedMentionsBuilder.() -> Unit = {}) { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(block) + } + + @OptIn(ExperimentalContracts::class) + @KordPreview + inline fun actionRow(builder: ActionRowBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + components.add(ActionRowBuilder().apply(builder)) + } + + @OptIn(ExperimentalContracts::class) + inline fun embed(builder: EmbedBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + embeds.add(EmbedBuilder().apply(builder).toRequest()) + } override fun toRequest(): MultipartFollowupMessageCreateRequest = MultipartFollowupMessageCreateRequest( FollowupMessageCreateRequest( content = _content, tts = _tts, - embeds = Optional.missingOnEmpty(embeds.map(EmbedBuilder::toRequest)), + embeds = Optional.missingOnEmpty(embeds), allowedMentions = _allowedMentions.map { it.build() }, components = Optional.missingOnEmpty(components.map(MessageComponentBuilder::build)) ), @@ -209,32 +207,46 @@ class PublicFollowupMessageCreateBuilder : FollowupMessageBuilder { - - private var _content: Optional = Optional.Missing() - override var content: String? by ::_content.delegate() +class EphemeralFollowupMessageCreateBuilder(var content: String) : + RequestBuilder { private var _tts: OptionalBoolean = OptionalBoolean.Missing var tts: Boolean? by ::_tts.delegate() private var _allowedMentions: Optional = Optional.Missing() - override var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() - - override val embeds: MutableList = mutableListOf() + var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() + + val components: MutableList = mutableListOf() + + /** + * Configures the mentions that should trigger a mention (aka ping). Not calling this function will result in the default behavior + * (ping everything), calling this function but not configuring it before the request is build will result in all + * pings being ignored. + */ + @OptIn(ExperimentalContracts::class) + inline fun allowedMentions(block: AllowedMentionsBuilder.() -> Unit = {}) { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(block) + } - override val components: MutableList = mutableListOf() + @OptIn(ExperimentalContracts::class) + @KordPreview + inline fun actionRow(builder: ActionRowBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + components.add(ActionRowBuilder().apply(builder)) + } override fun toRequest(): MultipartFollowupMessageCreateRequest = MultipartFollowupMessageCreateRequest( FollowupMessageCreateRequest( - content = _content, - embeds = Optional.missingOnEmpty(embeds.map(EmbedBuilder::toRequest)), + content = Optional.Value(content), tts = _tts, allowedMentions = _allowedMentions.map { it.build() }, components = Optional.missingOnEmpty(components.map(MessageComponentBuilder::build)) ), ) - } diff --git a/rest/src/main/kotlin/builder/interaction/OptionsBuilder.kt b/rest/src/main/kotlin/builder/interaction/OptionsBuilder.kt index 936843c03ca4..a0542b56fda9 100644 --- a/rest/src/main/kotlin/builder/interaction/OptionsBuilder.kt +++ b/rest/src/main/kotlin/builder/interaction/OptionsBuilder.kt @@ -101,6 +101,7 @@ class RoleBuilder(name: String, description: String) : class ChannelBuilder(name: String, description: String) : OptionsBuilder(name, description, ApplicationCommandOptionType.Channel) +@KordDsl @KordPreview class MentionableBuilder(name: String, description: String) : OptionsBuilder(name, description, ApplicationCommandOptionType.Mentionable) diff --git a/rest/src/main/kotlin/builder/interaction/PublicInteractionBuilder.kt b/rest/src/main/kotlin/builder/interaction/PublicInteractionBuilder.kt index 5440e66c2e33..b3d6f5910156 100644 --- a/rest/src/main/kotlin/builder/interaction/PublicInteractionBuilder.kt +++ b/rest/src/main/kotlin/builder/interaction/PublicInteractionBuilder.kt @@ -8,6 +8,8 @@ import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.common.entity.optional.delegate.delegate import dev.kord.common.entity.optional.map import dev.kord.common.entity.optional.optional +import dev.kord.rest.builder.component.ActionRowBuilder +import dev.kord.rest.builder.component.MessageComponentBuilder import dev.kord.rest.builder.message.AllowedMentionsBuilder import dev.kord.rest.builder.message.EmbedBuilder import dev.kord.rest.json.request.* @@ -16,11 +18,14 @@ import kotlinx.coroutines.withContext import java.io.InputStream import java.nio.file.Files import java.nio.file.Path +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract @KordPreview @KordDsl class PublicInteractionResponseCreateBuilder : - BaseInteractionResponseCreateBuilder { + BaseInteractionResponseCreateBuilder { private var _content: Optional = Optional.Missing() override var content: String? by ::_content.delegate() @@ -33,9 +38,40 @@ class PublicInteractionResponseCreateBuilder : private var _tts: OptionalBoolean = OptionalBoolean.Missing var tts: Boolean? by ::_tts.delegate() + @KordPreview + val components: MutableList = mutableListOf() + val files: MutableList> = mutableListOf() + /** + * Configures the mentions that should trigger a mention (aka ping). Not calling this function will result in the default behavior + * (ping everything), calling this function but not configuring it before the request is build will result in all + * pings being ignored. + */ + @OptIn(ExperimentalContracts::class) + inline fun allowedMentions(block: AllowedMentionsBuilder.() -> Unit = {}) { + contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } + allowedMentions = (allowedMentions ?: AllowedMentionsBuilder()).apply(block) + } + + @OptIn(ExperimentalContracts::class) + inline fun embed(builder: EmbedBuilder.() -> Unit) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + if (embeds == null) embeds = mutableListOf() + embeds!! += EmbedBuilder().apply(builder) + } + + @OptIn(ExperimentalContracts::class) + @KordPreview + inline fun actionRow(builder: ActionRowBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + components.add(ActionRowBuilder().apply(builder)) + } + fun addFile(name: String, content: InputStream) { files += name to content } @@ -51,12 +87,13 @@ class PublicInteractionResponseCreateBuilder : return MultipartInteractionResponseCreateRequest( InteractionResponseCreateRequest( - type, + type, InteractionApplicationCommandCallbackData( content = _content, - embeds = embeds.map { it.toRequest() }, + embeds = Optional.missingOnEmpty(embeds.map { it.toRequest() }), allowedMentions = _allowedMentions.map { it.build() }, - tts = _tts + tts = _tts, + components = Optional.missingOnEmpty(components.map { it.build() }) ).optional() ), files @@ -72,17 +109,29 @@ class PublicInteractionResponseModifyBuilder : private var _content: Optional = Optional.Missing() override var content: String? by ::_content.delegate() - override var embeds: MutableList = mutableListOf() + override val embeds: MutableList = mutableListOf() private var _allowedMentions: Optional = Optional.Missing() override var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() val files: MutableList> = mutableListOf() + val components: MutableList = mutableListOf() + fun addFile(name: String, content: InputStream) { files += name to content } + @OptIn(ExperimentalContracts::class) + @KordPreview + inline fun actionRow(builder: ActionRowBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + components.add(ActionRowBuilder().apply(builder)) + } + suspend fun addFile(path: Path) = withContext(Dispatchers.IO) { addFile(path.fileName.toString(), Files.newInputStream(path)) } @@ -93,6 +142,7 @@ class PublicInteractionResponseModifyBuilder : content = _content, embeds = embeds.map { it.toRequest() }, allowedMentions = _allowedMentions.map { it.build() }, + components = Optional.missingOnEmpty(components.map(MessageComponentBuilder::build)) ), files ) diff --git a/rest/src/main/kotlin/builder/message/MessageModifyBuilder.kt b/rest/src/main/kotlin/builder/message/MessageModifyBuilder.kt index b9ee418fd2de..b75b7721655d 100644 --- a/rest/src/main/kotlin/builder/message/MessageModifyBuilder.kt +++ b/rest/src/main/kotlin/builder/message/MessageModifyBuilder.kt @@ -5,7 +5,6 @@ import dev.kord.common.annotation.KordPreview import dev.kord.common.entity.MessageFlags import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.delegate.delegate -import dev.kord.common.entity.optional.mapList import dev.kord.common.entity.optional.mapNullable import dev.kord.rest.builder.RequestBuilder import dev.kord.rest.builder.component.ActionRowBuilder @@ -30,11 +29,8 @@ class MessageModifyBuilder : RequestBuilder { private var _allowedMentions: Optional = Optional.Missing() var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() - @OptIn(KordPreview::class) - private var _components: Optional> = Optional.Missing() - @KordPreview - var components: MutableList? by ::_components.delegate() + val components: MutableList = mutableListOf() @OptIn(ExperimentalContracts::class) inline fun embed(block: EmbedBuilder.() -> Unit) { @@ -61,9 +57,8 @@ class MessageModifyBuilder : RequestBuilder { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - components = (components ?: mutableListOf()).also { - it.add(ActionRowBuilder().apply(builder)) - } + + components.add(ActionRowBuilder().apply(builder)) } @OptIn(KordPreview::class) @@ -72,6 +67,6 @@ class MessageModifyBuilder : RequestBuilder { _embed.mapNullable { it?.toRequest() }, _flags, _allowedMentions.mapNullable { it?.build() }, - _components.mapList { it.build() } + Optional.missingOnEmpty(components.map(MessageComponentBuilder::build)) ) } diff --git a/rest/src/main/kotlin/json/request/InteractionsRequests.kt b/rest/src/main/kotlin/json/request/InteractionsRequests.kt index 1aff46e043b8..1917588a566f 100644 --- a/rest/src/main/kotlin/json/request/InteractionsRequests.kt +++ b/rest/src/main/kotlin/json/request/InteractionsRequests.kt @@ -35,7 +35,8 @@ data class InteractionResponseModifyRequest( val embeds: List = emptyList(), @SerialName("allowed_mentions") val allowedMentions: Optional = Optional.Missing(), - val flags: Optional = Optional.Missing() + val flags: Optional = Optional.Missing(), + val components: Optional> = Optional.Missing() ) @KordPreview @@ -62,11 +63,11 @@ data class MultipartInteractionResponseCreateRequest( class InteractionApplicationCommandCallbackData( val tts: OptionalBoolean = OptionalBoolean.Missing, val content: Optional = Optional.Missing(), - val embeds: List = emptyList(), + val embeds: Optional> = Optional.Missing(), @SerialName("allowed_mentions") val allowedMentions: Optional = Optional.Missing(), - val flags: Optional = Optional.Missing() - + val flags: Optional = Optional.Missing(), + val components: Optional> = Optional.Missing() ) @KordPreview @@ -84,7 +85,8 @@ class FollowupMessageCreateRequest( val avatar: Optional = Optional.Missing(), val tts: OptionalBoolean = OptionalBoolean.Missing, val embeds: Optional> = Optional.Missing(), - val allowedMentions: Optional = Optional.Missing() + val allowedMentions: Optional = Optional.Missing(), + val components: Optional> = Optional.Missing() ) @Serializable @@ -94,6 +96,7 @@ data class FollowupMessageModifyRequest( val embeds: Optional> = Optional.Missing(), @SerialName("allowed_mentions") val allowedMentions: Optional = Optional.Missing(), + val components: Optional> = Optional.Missing() ) @KordPreview