From f1aecb7da698dbe115446525a4b994be7ffe0ade Mon Sep 17 00:00:00 2001 From: Lukellmann <47486203+Lukellmann@users.noreply.github.com> Date: Tue, 1 Feb 2022 12:10:10 +0100 Subject: [PATCH] Add missing Webhook and Interaction functionality (#507) * No shards field on GatewayResponse * Fix authentication header requirements * inline shorthands in suppliers * reason null by default everywhere * Add routes for webhooks and followup messages * Add endpoints in WebhookService * use createGlobalMessageCommandApplicationCommand in Kord * add getFollowupMessage() to InteractionService, rename createPublicInteractionResponse() * use builder from InteractionService in ActionInteractionBehavior * use builder from InteractionService in FollowupMessageBehavior * use builder from InteractionService in InteractionResponseBehavior * use builder from InteractionService in PublicFollowupMessageBehavior * add getFollowupMessageOrNull() and getFollowupMessage() * getFollowupMessage() in EntitySupplier * move withStrategy() to interfaces * get, edit and delete functions for webhook messages * Restore explicit api after merge * formatting * Rename InteractionFollowup to FollowupMessage and better typing for editing followups --- core/src/main/kotlin/Kord.kt | 4 +- .../main/kotlin/behavior/MessageBehavior.kt | 60 ++++--- .../main/kotlin/behavior/WebhookBehavior.kt | 80 +++++++-- .../interaction/ActionInteractionBehavior.kt | 38 +++-- .../EphemeralFollowupMessageBehavior.kt | 22 +++ .../EphemeralInteractionResponseBehavior.kt | 23 ++- .../interaction/FollowupMessageBehavior.kt | 26 ++- .../InteractionResponseBehavior.kt | 47 ++++-- .../PublicFollowupMessageBehavior.kt | 9 +- .../PublicInteractionResponseBehavior.kt | 18 +- core/src/main/kotlin/entity/Message.kt | 4 +- .../entity/interaction/ContextInteraction.kt | 6 +- ...eractionFollowup.kt => FollowupMessage.kt} | 34 ++-- .../event/interaction/ApplicationCreate.kt | 6 +- .../exception/EntityNotFoundException.kt | 25 ++- .../kotlin/supplier/CacheEntitySupplier.kt | 29 ++++ .../main/kotlin/supplier/EntitySupplier.kt | 59 +++++++ .../kotlin/supplier/FallbackEntitySupplier.kt | 16 ++ .../kotlin/supplier/RestEntitySupplier.kt | 36 +++- .../kotlin/supplier/StoreEntitySupplier.kt | 20 +++ core/src/test/kotlin/rest/RestTest.kt | 2 +- rest/src/main/kotlin/route/Route.kt | 26 ++- .../main/kotlin/service/InteractionService.kt | 157 +++++++++++------- .../src/main/kotlin/service/WebhookService.kt | 83 ++++++--- 24 files changed, 616 insertions(+), 214 deletions(-) rename core/src/main/kotlin/entity/interaction/{InteractionFollowup.kt => FollowupMessage.kt} (66%) diff --git a/core/src/main/kotlin/Kord.kt b/core/src/main/kotlin/Kord.kt index a4510ebf644a..5d4e703571a0 100644 --- a/core/src/main/kotlin/Kord.kt +++ b/core/src/main/kotlin/Kord.kt @@ -486,8 +486,8 @@ public class Kord( builder: MessageCommandCreateBuilder.() -> Unit = {}, ): GlobalMessageCommand { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val request = MessageCommandCreateBuilder(name).apply(builder).toRequest() - val response = rest.interaction.createGlobalApplicationCommand(resources.applicationId, request) + val response = + rest.interaction.createGlobalMessageCommandApplicationCommand(resources.applicationId, name, builder) val data = ApplicationCommandData.from(response) return GlobalMessageCommand(data, rest.interaction) } diff --git a/core/src/main/kotlin/behavior/MessageBehavior.kt b/core/src/main/kotlin/behavior/MessageBehavior.kt index eca2263048e8..737f1dbc1d8b 100644 --- a/core/src/main/kotlin/behavior/MessageBehavior.kt +++ b/core/src/main/kotlin/behavior/MessageBehavior.kt @@ -14,7 +14,6 @@ import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull -import dev.kord.rest.builder.message.create.MessageCreateBuilder import dev.kord.rest.builder.message.create.UserMessageCreateBuilder import dev.kord.rest.builder.message.modify.UserMessageModifyBuilder import dev.kord.rest.builder.message.modify.WebhookMessageModifyBuilder @@ -47,10 +46,10 @@ public interface MessageBehavior : KordEntity, Strategizable { public suspend fun getChannelOrNull(): MessageChannel? = supplier.getChannelOfOrNull(channelId) /** - * Requests to get the this behavior as a [Message]. + * Requests to get this behavior as a [Message]. * - * @throws [RequestException] if anything went wrong during the request. - * @throws [EntityNotFoundException] if the message wasn't present. + * @throws RequestException if anything went wrong during the request. + * @throws EntityNotFoundException if the message wasn't present. */ public suspend fun asMessage(): Message = supplier.getMessage(channelId = channelId, messageId = id) @@ -89,6 +88,18 @@ public interface MessageBehavior : KordEntity, Strategizable { kord.rest.channel.deleteMessage(channelId = channelId, messageId = id, reason = reason) } + /** + * Requests to delete this message if it was previously sent from a [Webhook] with the given [webhookId] using the + * [token] for authentication. + * + * If this message is in a thread, [threadId] must be specified. + * + * @throws RestRequestException if something went wrong during the request. + */ + public suspend fun delete(webhookId: Snowflake, token: String, threadId: Snowflake? = null) { + kord.rest.webhook.deleteWebhookMessage(webhookId, token, messageId = id, threadId) + } + /** * Requests to get all users that have reacted to this message. * @@ -235,8 +246,7 @@ public fun MessageBehavior( * * @return The edited [Message]. * - * @throws [RestRequestException] if something went wrong during the request. - * @see editWebhookMessage + * @throws RestRequestException if something went wrong during the request. */ public suspend inline fun MessageBehavior.edit(builder: UserMessageModifyBuilder.() -> Unit): Message { contract { @@ -250,37 +260,47 @@ public suspend inline fun MessageBehavior.edit(builder: UserMessageModifyBuilder return Message(data, kord) } +@Deprecated( + "'editWebhookMessage' was renamed to 'edit'", + ReplaceWith("this.edit(webhookId, token, threadId = null) { builder() }", "dev.kord.core.behavior.edit"), + DeprecationLevel.ERROR, +) +public suspend inline fun MessageBehavior.editWebhookMessage( + webhookId: Snowflake, + token: String, + builder: WebhookMessageModifyBuilder.() -> Unit, +): Message { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return edit(webhookId, token, threadId = null, builder) +} + /** - * Requests to edit this message. + * Requests to edit this message if it was previously sent from a [Webhook] with the given [webhookId] using the + * [token] for authentication. + * + * If this message is in a thread, [threadId] must be specified. * * @return The edited [Message]. * - * @throws [RestRequestException] if something went wrong during the request. - * @see edit + * @throws RestRequestException if something went wrong during the request. */ -public suspend inline fun MessageBehavior.editWebhookMessage( +public suspend inline fun MessageBehavior.edit( webhookId: Snowflake, token: String, + threadId: Snowflake? = null, builder: WebhookMessageModifyBuilder.() -> Unit ): Message { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - - val response = - kord.rest.webhook.editWebhookMessage( - webhookId = webhookId, - messageId = id, - token = token, - builder = builder - ) + val response = kord.rest.webhook.editWebhookMessage(webhookId, token, messageId = id, threadId, builder) val data = MessageData.from(response) - return Message(data, kord) } /** - * Request to reply to this message, setting [MessageCreateBuilder.messageReference] to this message [id][MessageBehavior.id]. + * Request to reply to this message, setting [messageReference][UserMessageCreateBuilder.messageReference] to this + * message's [id][MessageBehavior.id]. * * @throws [RestRequestException] if something went wrong during the request. */ diff --git a/core/src/main/kotlin/behavior/WebhookBehavior.kt b/core/src/main/kotlin/behavior/WebhookBehavior.kt index 18a333e618e7..b70f37fca7cc 100644 --- a/core/src/main/kotlin/behavior/WebhookBehavior.kt +++ b/core/src/main/kotlin/behavior/WebhookBehavior.kt @@ -1,6 +1,7 @@ package dev.kord.core.behavior import dev.kord.common.entity.Snowflake +import dev.kord.common.exception.RequestException import dev.kord.core.Kord import dev.kord.core.cache.data.MessageData import dev.kord.core.cache.data.WebhookData @@ -8,6 +9,7 @@ import dev.kord.core.entity.KordEntity import dev.kord.core.entity.Message import dev.kord.core.entity.Strategizable import dev.kord.core.entity.Webhook +import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.rest.builder.message.create.WebhookMessageCreateBuilder @@ -26,7 +28,7 @@ public interface WebhookBehavior : KordEntity, Strategizable { * Requests to delete this webhook, this user must be the creator. * * @param reason the reason showing up in the audit log - * @throws [RestRequestException] if something went wrong during the request. + * @throws RestRequestException if something went wrong during the request. */ public suspend fun delete(reason: String? = null) { kord.rest.webhook.deleteWebhook(id, reason) @@ -36,12 +38,53 @@ public interface WebhookBehavior : KordEntity, Strategizable { * Requests to delete this webhook. * * @param reason the reason showing up in the audit log - * @throws [RestRequestException] if something went wrong during the request. + * @throws RestRequestException if something went wrong during the request. */ public suspend fun delete(token: String, reason: String? = null) { kord.rest.webhook.deleteWebhookWithToken(id, token, reason) } + /** + * Requests the [Message] with the given [messageId] previously sent from this webhook using the [token] for + * authentication, returns `null` when the message isn't present. + * + * If the message is in a thread, [threadId] must be specified. + * + * @throws RequestException if something went wrong while retrieving the message. + */ + public suspend fun getMessageOrNull( + token: String, + messageId: Snowflake, + threadId: Snowflake? = null, + ): Message? = supplier.getWebhookMessageOrNull(id, token, messageId, threadId) + + /** + * Requests the [Message] with the given [messageId] previously sent from this webhook using the [token] for + * authentication. + * + * If the message is in a thread, [threadId] must be specified. + * + * @throws RequestException if something went wrong while retrieving the message. + * @throws EntityNotFoundException if the message is null. + */ + public suspend fun getMessage( + token: String, + messageId: Snowflake, + threadId: Snowflake? = null, + ): Message = supplier.getWebhookMessage(id, token, messageId, threadId) + + /** + * Requests to delete the [Message] with the given [messageId] previously sent from this webhook using the [token] + * for authentication. + * + * If the message is in a thread, [threadId] must be specified. + * + * @throws RestRequestException if something went wrong during the request. + */ + public suspend fun deleteMessage(token: String, messageId: Snowflake, threadId: Snowflake? = null) { + kord.rest.webhook.deleteWebhookMessage(id, token, messageId, threadId) + } + /** * Returns a new [WebhookBehavior] with the given [strategy]. */ @@ -108,23 +151,21 @@ public suspend inline fun WebhookBehavior.edit(token: String, builder: WebhookMo /** * Requests to execute this webhook. - * if [threadId] is specified the execution will occur in that thread. + * + * If [threadId] is specified the execution will occur in that thread. + * * @throws [RestRequestException] if something went wrong during the request. */ -public suspend inline fun WebhookBehavior.execute(token: String, threadId: Snowflake? = null, builder: WebhookMessageCreateBuilder.() -> Unit): Message { +public suspend inline fun WebhookBehavior.execute( + token: String, + threadId: Snowflake? = null, + builder: WebhookMessageCreateBuilder.() -> Unit, +): Message { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val response = kord.rest.webhook.executeWebhook( - token = token, - webhookId = id, - wait = true, - threadId = threadId, - builder = builder - )!! - + val response = kord.rest.webhook.executeWebhook(id, token, wait = true, threadId, builder)!! val data = MessageData.from(response) - return Message(data, kord) } @@ -133,11 +174,18 @@ public suspend inline fun WebhookBehavior.execute(token: String, threadId: Snowf * * This is a 'fire and forget' variant of [execute]. It will not wait for a response and might not throw an * Exception if the request wasn't executed. - * if [threadId] is specified the execution will occur in that thread. + * + * If [threadId] is specified the execution will occur in that thread. + * + * @throws [RestRequestException] if something went wrong during the request. */ -public suspend inline fun WebhookBehavior.executeIgnored(token: String, threadId: Snowflake? = null, builder: WebhookMessageCreateBuilder.() -> Unit) { +public suspend inline fun WebhookBehavior.executeIgnored( + token: String, + threadId: Snowflake? = null, + builder: WebhookMessageCreateBuilder.() -> Unit, +) { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - kord.rest.webhook.executeWebhook(token = token, webhookId = id, wait = false, threadId = threadId, builder = builder) + kord.rest.webhook.executeWebhook(id, token, wait = false, threadId, builder) } diff --git a/core/src/main/kotlin/behavior/interaction/ActionInteractionBehavior.kt b/core/src/main/kotlin/behavior/interaction/ActionInteractionBehavior.kt index c64f0353cf3e..d97155eb18bb 100644 --- a/core/src/main/kotlin/behavior/interaction/ActionInteractionBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/ActionInteractionBehavior.kt @@ -3,9 +3,12 @@ package dev.kord.core.behavior.interaction import dev.kord.common.entity.Snowflake import dev.kord.core.Kord import dev.kord.core.entity.Message +import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kord.core.supplier.EntitySupplyStrategy.Companion.rest import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder +import dev.kord.rest.request.RestRequestException import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -21,7 +24,7 @@ public interface ActionInteractionBehavior : InteractionBehavior { * @return [EphemeralInteractionResponseBehavior] Ephemeral acknowledgement of the interaction. */ public suspend fun acknowledgeEphemeral(): EphemeralInteractionResponseBehavior { - kord.rest.interaction.acknowledge(id, token, true) + kord.rest.interaction.acknowledge(id, token, ephemeral = true) return EphemeralInteractionResponseBehavior(applicationId, token, kord) } @@ -31,13 +34,26 @@ public interface ActionInteractionBehavior : InteractionBehavior { * @return [PublicInteractionResponseBehavior] public acknowledgement of an interaction. */ public suspend fun acknowledgePublic(): PublicInteractionResponseBehavior { - kord.rest.interaction.acknowledge(id, token) + kord.rest.interaction.acknowledge(id, token, ephemeral = false) return PublicInteractionResponseBehavior(applicationId, token, kord) } - public suspend fun getOriginalInteractionResponse(): Message? { - return EntitySupplyStrategy.rest.supply(kord).getOriginalInteractionOrNull(applicationId, token) - } + /** + * Returns the initial interaction response or `null` if it was not found. + * + * @throws RestRequestException if something went wrong during the request. + */ + public suspend fun getOriginalInteractionResponseOrNull(): Message? = + kord.with(rest).getOriginalInteractionOrNull(applicationId, token) + + /** + * Returns the initial interaction response. + * + * @throws RestRequestException if something went wrong during the request. + * @throws EntityNotFoundException if the initial interaction response was not found. + */ + public suspend fun getOriginalInteractionResponse(): Message = + kord.with(rest).getOriginalInteraction(applicationId, token) } @@ -50,13 +66,9 @@ public interface ActionInteractionBehavior : InteractionBehavior { public suspend inline fun ActionInteractionBehavior.respondPublic( builder: InteractionResponseCreateBuilder.() -> Unit ): PublicInteractionResponseBehavior { - contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - - val request = InteractionResponseCreateBuilder().apply(builder).toRequest() - kord.rest.interaction.createInteractionResponse(id, token, request) + kord.rest.interaction.createInteractionResponse(id, token, ephemeral = false, builder) return PublicInteractionResponseBehavior(applicationId, token, kord) - } @@ -69,13 +81,9 @@ public suspend inline fun ActionInteractionBehavior.respondPublic( public suspend inline fun ActionInteractionBehavior.respondEphemeral( builder: InteractionResponseCreateBuilder.() -> Unit ): EphemeralInteractionResponseBehavior { - contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val builder = InteractionResponseCreateBuilder(true).apply(builder) - val request = builder.toRequest() - kord.rest.interaction.createInteractionResponse(id, token, request) + kord.rest.interaction.createInteractionResponse(id, token, ephemeral = true, builder) return EphemeralInteractionResponseBehavior(applicationId, token, kord) - } public fun InteractionBehavior( diff --git a/core/src/main/kotlin/behavior/interaction/EphemeralFollowupMessageBehavior.kt b/core/src/main/kotlin/behavior/interaction/EphemeralFollowupMessageBehavior.kt index ddf8105f94e7..9b30002a9177 100644 --- a/core/src/main/kotlin/behavior/interaction/EphemeralFollowupMessageBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/EphemeralFollowupMessageBehavior.kt @@ -2,8 +2,15 @@ package dev.kord.core.behavior.interaction import dev.kord.common.entity.Snowflake import dev.kord.core.Kord +import dev.kord.core.cache.data.toData +import dev.kord.core.entity.Message +import dev.kord.core.entity.interaction.EphemeralFollowupMessage import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kord.rest.builder.message.modify.FollowupMessageModifyBuilder +import dev.kord.rest.request.RestRequestException +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract /** * The behavior of a [Discord Followup Message](https://discord.com/developers/docs/interactions/slash-commands#followup-messages) @@ -17,6 +24,21 @@ public interface EphemeralFollowupMessageBehavior : FollowupMessageBehavior { } } +/** + * Requests to edit this followup message. + * + * @return The edited [EphemeralFollowupMessage] of the interaction response. + * + * @throws RestRequestException if something went wrong during the request. + */ +public suspend inline fun EphemeralFollowupMessageBehavior.edit( + builder: FollowupMessageModifyBuilder.() -> Unit, +): EphemeralFollowupMessage { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + val response = kord.rest.interaction.modifyFollowupMessage(applicationId, token, id, builder) + return EphemeralFollowupMessage(Message(response.toData(), kord), applicationId, token, kord) +} + public fun EphemeralFollowupMessageBehavior( id: Snowflake, diff --git a/core/src/main/kotlin/behavior/interaction/EphemeralInteractionResponseBehavior.kt b/core/src/main/kotlin/behavior/interaction/EphemeralInteractionResponseBehavior.kt index f28e81ac1072..5c07ae7054a2 100644 --- a/core/src/main/kotlin/behavior/interaction/EphemeralInteractionResponseBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/EphemeralInteractionResponseBehavior.kt @@ -2,27 +2,32 @@ package dev.kord.core.behavior.interaction import dev.kord.common.entity.Snowflake import dev.kord.core.Kord +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy /** - * The behavior of a ephemeral [Discord ActionInteraction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) + * The behavior of an ephemeral [Discord ActionInteraction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) * This response is visible to *only* to the user who made the interaction. */ +public interface EphemeralInteractionResponseBehavior : InteractionResponseBehavior { -public interface EphemeralInteractionResponseBehavior : InteractionResponseBehavior + override fun withStrategy(strategy: EntitySupplyStrategy<*>): EphemeralInteractionResponseBehavior = + EphemeralInteractionResponseBehavior(applicationId, token, kord, strategy) +} public fun EphemeralInteractionResponseBehavior( applicationId: Snowflake, token: String, - kord: Kord + kord: Kord, + strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy, ): EphemeralInteractionResponseBehavior = object : EphemeralInteractionResponseBehavior { - override val applicationId: Snowflake - get() = applicationId + override val applicationId: Snowflake = applicationId - override val token: String - get() = token + override val token: String = token - override val kord: Kord - get() = kord + override val kord: Kord = kord + + override val supplier: EntitySupplier = strategy.supply(kord) } diff --git a/core/src/main/kotlin/behavior/interaction/FollowupMessageBehavior.kt b/core/src/main/kotlin/behavior/interaction/FollowupMessageBehavior.kt index ad55a2360099..c2910f176574 100644 --- a/core/src/main/kotlin/behavior/interaction/FollowupMessageBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/FollowupMessageBehavior.kt @@ -1,5 +1,6 @@ package dev.kord.core.behavior.interaction +import dev.kord.common.entity.MessageFlag import dev.kord.common.entity.Snowflake import dev.kord.core.behavior.channel.MessageChannelBehavior import dev.kord.core.cache.data.toData @@ -8,6 +9,7 @@ import dev.kord.core.entity.Message import dev.kord.core.entity.Strategizable import dev.kord.core.entity.channel.MessageChannel import dev.kord.core.entity.interaction.EphemeralFollowupMessage +import dev.kord.core.entity.interaction.FollowupMessage import dev.kord.core.entity.interaction.PublicFollowupMessage import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull @@ -17,9 +19,8 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** - * The behavior of a [Discord Followup Message](https://discord.com/developers/docs/interactions/slash-commands#followup-messages) + * The behavior of a [Followup Message](https://discord.com/developers/docs/interactions/receiving-and-responding#followup-messages) */ - public interface FollowupMessageBehavior : KordEntity, Strategizable { public val applicationId: Snowflake @@ -37,13 +38,22 @@ public interface FollowupMessageBehavior : KordEntity, Strategizable { /** * Requests to edit this followup message. * - * @return The edited [PublicFollowupMessage] of the interaction response. + * @return The edited [FollowupMessage] of the interaction response, either [public][PublicFollowupMessage] or + * [ephemeral][EphemeralFollowupMessage]. * - * @throws [RestRequestException] if something went wrong during the request. + * @throws RestRequestException if something went wrong during the request. */ -public suspend inline fun FollowupMessageBehavior.edit(builder: FollowupMessageModifyBuilder.() -> Unit): EphemeralFollowupMessage { +public suspend inline fun FollowupMessageBehavior.edit( + builder: FollowupMessageModifyBuilder.() -> Unit, +): FollowupMessage { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val builder = FollowupMessageModifyBuilder().apply(builder) - val response = kord.rest.interaction.modifyFollowupMessage(applicationId, token, id, builder.toRequest()) - return EphemeralFollowupMessage(Message(response.toData(), kord), applicationId, token, kord) + val response = kord.rest.interaction.modifyFollowupMessage(applicationId, token, id, builder) + + val isEphemeral = response.flags.value?.contains(MessageFlag.Ephemeral) ?: false + val message = Message(response.toData(), kord) + + return when { + isEphemeral -> EphemeralFollowupMessage(message, applicationId, token, kord) + else -> PublicFollowupMessage(message, applicationId, token, kord) + } } diff --git a/core/src/main/kotlin/behavior/interaction/InteractionResponseBehavior.kt b/core/src/main/kotlin/behavior/interaction/InteractionResponseBehavior.kt index 405e1263802f..74bcd9d2cdf1 100644 --- a/core/src/main/kotlin/behavior/interaction/InteractionResponseBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/InteractionResponseBehavior.kt @@ -4,8 +4,10 @@ import dev.kord.common.entity.Snowflake import dev.kord.core.KordObject import dev.kord.core.cache.data.toData import dev.kord.core.entity.Message +import dev.kord.core.entity.Strategizable import dev.kord.core.entity.interaction.EphemeralFollowupMessage import dev.kord.core.entity.interaction.PublicFollowupMessage +import dev.kord.core.exception.EntityNotFoundException import dev.kord.rest.builder.message.create.FollowupMessageCreateBuilder import dev.kord.rest.builder.message.modify.InteractionResponseModifyBuilder import dev.kord.rest.request.RestRequestException @@ -13,34 +15,46 @@ import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** - * The behavior of a [Discord ActionInteraction Response](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) + * The behavior of a [Discord ActionInteraction Response](https://discord.com/developers/docs/interactions/receiving-and-responding#responding-to-an-interaction) */ - -public interface InteractionResponseBehavior : KordObject { +public interface InteractionResponseBehavior : KordObject, Strategizable { public val applicationId: Snowflake public val token: String + /** + * Returns a followup message for an interaction response or `null` if it was not found. Does not support ephemeral + * followups. + * + * @throws RestRequestException if something went wrong during the request. + */ + public suspend fun getFollowupMessageOrNull(messageId: Snowflake): PublicFollowupMessage? = + supplier.getFollowupMessageOrNull(applicationId, token, messageId) + + /** + * Returns a followup message for an interaction response. Does not support ephemeral followups. + * + * @throws RestRequestException if something went wrong during the request. + * @throws EntityNotFoundException if the followup message was not found. + */ + public suspend fun getFollowupMessage(messageId: Snowflake): PublicFollowupMessage = + supplier.getFollowupMessage(applicationId, token, messageId) } /** - * Follows up an interaction response without the [Ephemeral flag][dev.kord.common.entity.MessageFlag.Ephemeral] + * Follows up an interaction response without the [Ephemeral flag][dev.kord.common.entity.MessageFlag.Ephemeral]. */ public suspend inline fun InteractionResponseBehavior.followUp(builder: FollowupMessageCreateBuilder.() -> Unit): PublicFollowupMessage { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val builder = FollowupMessageCreateBuilder(false).apply(builder) - val message = kord.rest.interaction.createFollowupMessage(applicationId, token, builder.toRequest()) + val message = kord.rest.interaction.createFollowupMessage(applicationId, token, ephemeral = false, builder) return PublicFollowupMessage(Message(message.toData(), kord), applicationId, token, kord) } - /** - * Follows up an interaction response with the [Ephemeral flag][dev.kord.common.entity.MessageFlag.Ephemeral] - * + * Follows up an interaction response with the [Ephemeral flag][dev.kord.common.entity.MessageFlag.Ephemeral]. */ public suspend inline fun InteractionResponseBehavior.followUpEphemeral(builder: FollowupMessageCreateBuilder.() -> Unit): EphemeralFollowupMessage { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val builder = FollowupMessageCreateBuilder(true).apply(builder) - val message = kord.rest.interaction.createFollowupMessage(applicationId, token, builder.toRequest()) + val message = kord.rest.interaction.createFollowupMessage(applicationId, token, ephemeral = true, builder) return EphemeralFollowupMessage(Message(message.toData(), kord), applicationId, token, kord) } @@ -49,11 +63,12 @@ public suspend inline fun InteractionResponseBehavior.followUpEphemeral(builder: * * @return The edited [Message] of the interaction response. * - * @throws [RestRequestException] if something went wrong during the request. + * @throws RestRequestException if something went wrong during the request. */ - -public suspend inline fun InteractionResponseBehavior.edit(builder: InteractionResponseModifyBuilder.() -> Unit) { +public suspend inline fun InteractionResponseBehavior.edit( + builder: InteractionResponseModifyBuilder.() -> Unit, +): Message { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val builder = InteractionResponseModifyBuilder().apply(builder) - kord.rest.interaction.modifyInteractionResponse(applicationId, token, builder.toRequest()) + val message = kord.rest.interaction.modifyInteractionResponse(applicationId, token, builder) + return Message(message.toData(), kord) } diff --git a/core/src/main/kotlin/behavior/interaction/PublicFollowupMessageBehavior.kt b/core/src/main/kotlin/behavior/interaction/PublicFollowupMessageBehavior.kt index 3ee3553d8567..90a27f382929 100644 --- a/core/src/main/kotlin/behavior/interaction/PublicFollowupMessageBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/PublicFollowupMessageBehavior.kt @@ -39,12 +39,13 @@ public interface PublicFollowupMessageBehavior : FollowupMessageBehavior { * * @return The edited [PublicFollowupMessage] of the interaction response. * - * @throws [RestRequestException] if something went wrong during the request. + * @throws RestRequestException if something went wrong during the request. */ -public suspend inline fun PublicFollowupMessageBehavior.edit(builder: FollowupMessageModifyBuilder.() -> Unit): PublicFollowupMessage { +public suspend inline fun PublicFollowupMessageBehavior.edit( + builder: FollowupMessageModifyBuilder.() -> Unit, +): PublicFollowupMessage { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val builder = FollowupMessageModifyBuilder().apply(builder) - val response = kord.rest.interaction.modifyFollowupMessage(applicationId, token, id, builder.toRequest()) + val response = kord.rest.interaction.modifyFollowupMessage(applicationId, token, id, builder) return PublicFollowupMessage(Message(response.toData(), kord), applicationId, token, kord) } diff --git a/core/src/main/kotlin/behavior/interaction/PublicInteractionResponseBehavior.kt b/core/src/main/kotlin/behavior/interaction/PublicInteractionResponseBehavior.kt index 10ac9287ba1e..be742c5399a3 100644 --- a/core/src/main/kotlin/behavior/interaction/PublicInteractionResponseBehavior.kt +++ b/core/src/main/kotlin/behavior/interaction/PublicInteractionResponseBehavior.kt @@ -2,6 +2,8 @@ package dev.kord.core.behavior.interaction import dev.kord.common.entity.Snowflake import dev.kord.core.Kord +import dev.kord.core.supplier.EntitySupplier +import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.rest.request.RestRequestException @@ -21,21 +23,23 @@ public interface PublicInteractionResponseBehavior : InteractionResponseBehavior kord.rest.interaction.deleteOriginalInteractionResponse(applicationId, token) } + override fun withStrategy(strategy: EntitySupplyStrategy<*>): PublicInteractionResponseBehavior = + PublicInteractionResponseBehavior(applicationId, token, kord, strategy) } public fun PublicInteractionResponseBehavior( applicationId: Snowflake, token: String, - kord: Kord + kord: Kord, + strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy, ): PublicInteractionResponseBehavior = object : PublicInteractionResponseBehavior { - override val applicationId: Snowflake - get() = applicationId + override val applicationId: Snowflake = applicationId - override val token: String - get() = token + override val token: String = token - override val kord: Kord - get() = kord + override val kord: Kord = kord + + override val supplier: EntitySupplier = strategy.supply(kord) } diff --git a/core/src/main/kotlin/entity/Message.kt b/core/src/main/kotlin/entity/Message.kt index 0de2c5d79268..5d1b6c3b3518 100644 --- a/core/src/main/kotlin/entity/Message.kt +++ b/core/src/main/kotlin/entity/Message.kt @@ -18,7 +18,7 @@ import dev.kord.core.entity.channel.MessageChannel import dev.kord.core.entity.channel.TopGuildMessageChannel import dev.kord.core.entity.component.Component import dev.kord.core.entity.interaction.ActionInteraction -import dev.kord.core.entity.interaction.InteractionFollowup +import dev.kord.core.entity.interaction.FollowupMessage import dev.kord.core.entity.interaction.MessageInteraction import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplier @@ -62,7 +62,7 @@ public class Message( * The author of this message, if it was created by a [User]. * * Returns null if the author is not a Discord account, like a [Webhook] or systems message. This also applies to - * [interaction responses][InteractionResponseBehavior] and [followup messages][InteractionFollowup] since + * [interaction responses][InteractionResponseBehavior] and [followup messages][FollowupMessage] since * [they are webhooks under the hood](https://discord.com/developers/docs/interactions/receiving-and-responding#responding-to-an-interaction). */ public val author: User? diff --git a/core/src/main/kotlin/entity/interaction/ContextInteraction.kt b/core/src/main/kotlin/entity/interaction/ContextInteraction.kt index 03cf8cd4cfd5..136cac5f2a12 100644 --- a/core/src/main/kotlin/entity/interaction/ContextInteraction.kt +++ b/core/src/main/kotlin/entity/interaction/ContextInteraction.kt @@ -224,7 +224,7 @@ public class UnknownApplicationCommandInteraction( /** * ActionInteraction indicating an auto-complete request from Discord. * - * **Follow-ups and normals responses don't work on this type** + * **Followups and normals responses don't work on this type** * * **No matter what argument type is used all [focused][CommandArgument.focused] arguments will be [CommandArgument.AutoCompleteArgument]s** * @@ -246,7 +246,7 @@ internal fun AutoCompleteInteraction( /** * ActionInteraction indicating an auto-complete request from Discord. * - * **Follow-ups and normals responses don't work on this type** + * **Followups and normals responses don't work on this type** * * @see ApplicationCommandInteraction */ @@ -262,7 +262,7 @@ public class GlobalAutoCompleteInteraction( /** * ActionInteraction indicating an auto-complete request from Discord on a guild. * - * **Follow-ups and normals responses don't work on this type** + * **Followups and normals responses don't work on this type** * * @see ApplicationCommandInteraction */ diff --git a/core/src/main/kotlin/entity/interaction/InteractionFollowup.kt b/core/src/main/kotlin/entity/interaction/FollowupMessage.kt similarity index 66% rename from core/src/main/kotlin/entity/interaction/InteractionFollowup.kt rename to core/src/main/kotlin/entity/interaction/FollowupMessage.kt index 6952eeef19e6..c97550a684de 100644 --- a/core/src/main/kotlin/entity/interaction/InteractionFollowup.kt +++ b/core/src/main/kotlin/entity/interaction/FollowupMessage.kt @@ -10,23 +10,29 @@ import dev.kord.core.entity.channel.MessageChannel import dev.kord.core.supplier.EntitySupplier import dev.kord.core.supplier.EntitySupplyStrategy +@Deprecated( + "'InteractionFollowup' was renamed to 'FollowupMessage'.", + ReplaceWith("FollowupMessage", "dev.kord.core.entity.interaction.FollowupMessage"), + DeprecationLevel.ERROR, +) +public typealias InteractionFollowup = FollowupMessage + /** - * Holds the follow-up [Message] resulting from an interaction follow-up - * and behaves on it through [FollowupMessageBehavior] + * Holds the followup [Message] resulting from an interaction followup + * and behaves on it through [FollowupMessageBehavior]. * - * @param message The message created by this follow-up. + * @param message The message created by this followup. * To use the message behavior your application must be authorized as a bot. */ - -public sealed class InteractionFollowup(public val message: Message) : FollowupMessageBehavior { +public sealed class FollowupMessage(public val message: Message) : FollowupMessageBehavior { /** - * The id of the follow-up message. + * The id of the followup message. */ override val id: Snowflake get() = message.id /** - * The id of the [MessageChannel] the follow-up message was send in. + * The id of the [MessageChannel] the followup message was sent in. */ override val channelId: Snowflake get() = message.channelId @@ -35,20 +41,19 @@ public sealed class InteractionFollowup(public val message: Message) : FollowupM /** - * Holds the follow-up [Message] resulting from an public followup message + * Holds the followup [Message] resulting from a public followup message * and behaves on it through [PublicFollowupMessageBehavior] * - * @param message The message created by this follow-up. + * @param message The message created by this followup. * To use the message behavior your application must be authorized as a bot. */ - public class PublicFollowupMessage( message: Message, override val applicationId: Snowflake, override val token: String, override val kord: Kord, override val supplier: EntitySupplier = kord.defaultSupplier -) : InteractionFollowup(message), PublicFollowupMessageBehavior { +) : FollowupMessage(message), PublicFollowupMessageBehavior { override fun withStrategy(strategy: EntitySupplyStrategy<*>): PublicFollowupMessage { return PublicFollowupMessage(message, applicationId, token, kord, strategy.supply(kord)) @@ -57,22 +62,21 @@ public class PublicFollowupMessage( /** - * Holds the follow-up [Message] resulting from an ephemeral followup message + * Holds the followup [Message] resulting from an ephemeral followup message * and behaves on it through [EphemeralFollowupMessageBehavior]. * - * @param message The message created by this follow-up. + * @param message The message created by this followup. * To use the message behavior your application must be authorized as a bot. * Note: Any rest calls made through the [message] object e.g: `message.delete()` will throw since the message * is deleted once the client receives it. */ - public class EphemeralFollowupMessage( message: Message, override val applicationId: Snowflake, override val token: String, override val kord: Kord, override val supplier: EntitySupplier = kord.defaultSupplier -) : InteractionFollowup(message), EphemeralFollowupMessageBehavior { +) : FollowupMessage(message), EphemeralFollowupMessageBehavior { override fun withStrategy(strategy: EntitySupplyStrategy<*>): EphemeralFollowupMessage { return EphemeralFollowupMessage(message, applicationId, token, kord, strategy.supply(kord)) diff --git a/core/src/main/kotlin/event/interaction/ApplicationCreate.kt b/core/src/main/kotlin/event/interaction/ApplicationCreate.kt index 357e5f204e12..d5dd62e15670 100644 --- a/core/src/main/kotlin/event/interaction/ApplicationCreate.kt +++ b/core/src/main/kotlin/event/interaction/ApplicationCreate.kt @@ -28,12 +28,12 @@ import kotlinx.coroutines.CoroutineScope * Once an interaction has been acknowledged, * you can use [PublicInteractionResponseBehavior.followUp] or [EphemeralInteractionResponseBehavior.followUp] to display additional messages. * - * The resulting follow-up message and its methods may differ based on which method is used. - * * Following up an acknowledgement results in replacing "The bot is thinking" prompt with the follow-up content. + * The resulting followup message and its methods may differ based on which method is used. + * * Following up an acknowledgement results in replacing "The bot is thinking" prompt with the followup content. * * Following up a response results in a completely new message instance. * * As such, due to how Discord handles ephemeral acknowledgements, - * a follow-up on ephemeral acknowledgement will result in an ephemeral message. + * a followup on ephemeral acknowledgement will result in an ephemeral message. * * In the current iteration, ephemeral messages (regardless of the type) don't support files and/or embeds. */ diff --git a/core/src/main/kotlin/exception/EntityNotFoundException.kt b/core/src/main/kotlin/exception/EntityNotFoundException.kt index d877b1f375e9..70be3444c478 100644 --- a/core/src/main/kotlin/exception/EntityNotFoundException.kt +++ b/core/src/main/kotlin/exception/EntityNotFoundException.kt @@ -32,13 +32,13 @@ public class EntityNotFoundException : Exception { guildEntityNotFound("Member", guildId = guildId, id = userId) public inline fun messageNotFound(channelId: Snowflake, messageId: Snowflake): Nothing = - throw EntityNotFoundException("Message with id $messageId in channel $channelId was not found") + throw EntityNotFoundException("Message with id $messageId in channel $channelId was not found.") public inline fun userNotFound(userId: Snowflake): Nothing = entityNotFound("User", userId) public inline fun selfNotFound(): Nothing = - throw EntityNotFoundException("Self user not found") + throw EntityNotFoundException("Self user was not found.") public inline fun roleNotFound(guildId: Snowflake, roleId: Snowflake): Nothing = guildEntityNotFound("Role", guildId = guildId, id = roleId) @@ -52,6 +52,17 @@ public class EntityNotFoundException : Exception { public inline fun webhookNotFound(webhookId: Snowflake): Nothing = entityNotFound("Webhook", webhookId) + public inline fun webhookMessageNotFound( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake? = null, + ): Nothing = throw EntityNotFoundException( + "Message with id $messageId ${ + if (threadId != null) "in thread $threadId " else "" + }from webhook $webhookId with token $token was not found." + ) + public inline fun inviteNotFound(code: String): Nothing = throw EntityNotFoundException("Invite with code $code was not found.") @@ -79,7 +90,13 @@ public class EntityNotFoundException : Exception { public inline fun applicationCommandNotFound(commandId: Snowflake): Nothing = entityNotFound(T::class.simpleName!!, commandId) - public inline fun interactionNotFound(token: String): Nothing = - throw EntityNotFoundException("ActionInteraction with token $token was not found.") + public inline fun interactionNotFound(token: String): Nothing = throw EntityNotFoundException( + "Initial interaction response for interaction with token $token was not found." + ) + + public inline fun followupMessageNotFound(token: String, messageId: Snowflake): Nothing = + throw EntityNotFoundException( + "Followup message with id $messageId for interaction with token $token was not found." + ) } } diff --git a/core/src/main/kotlin/supplier/CacheEntitySupplier.kt b/core/src/main/kotlin/supplier/CacheEntitySupplier.kt index 87f900fa0dd3..3ac108cde974 100644 --- a/core/src/main/kotlin/supplier/CacheEntitySupplier.kt +++ b/core/src/main/kotlin/supplier/CacheEntitySupplier.kt @@ -20,6 +20,7 @@ import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.TopGuildChannel import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.entity.channel.thread.ThreadMember +import dev.kord.core.entity.interaction.PublicFollowupMessage import dev.kord.core.exception.EntityNotFoundException import dev.kord.gateway.Gateway import kotlinx.coroutines.flow.* @@ -261,6 +262,21 @@ public class CacheEntitySupplier(private val kord: Kord) : EntitySupplier { return Webhook(data, kord) } + override suspend fun getWebhookMessageOrNull( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake?, + ): Message? { + val data = cache.query { + idEq(MessageData::webhookId, webhookId) + idEq(MessageData::id, messageId) + if (threadId != null) idEq(MessageData::channelId, threadId) + }.singleOrNull() ?: return null + + return Message(data, kord) + } + override suspend fun getUserOrNull(id: Snowflake): User? { val data = cache.query { idEq(UserData::id, id) }.singleOrNull() ?: return null @@ -435,6 +451,19 @@ public class CacheEntitySupplier(private val kord: Kord) : EntitySupplier { return ApplicationCommandPermissions(data) } + override suspend fun getFollowupMessageOrNull( + applicationId: Snowflake, + interactionToken: String, + messageId: Snowflake, + ): PublicFollowupMessage? { + val data = cache.query { + idEq(MessageData::applicationId, applicationId) + idEq(MessageData::id, messageId) + }.singleOrNull() ?: return null + + return PublicFollowupMessage(Message(data, kord), applicationId, interactionToken, kord) + } + override suspend fun getGuildScheduledEventOrNull(guildId: Snowflake, eventId: Snowflake): GuildScheduledEvent? { val data = cache.query { idEq(GuildScheduledEventData::guildId, guildId) diff --git a/core/src/main/kotlin/supplier/EntitySupplier.kt b/core/src/main/kotlin/supplier/EntitySupplier.kt index 3856cbc837c2..06884f96c863 100644 --- a/core/src/main/kotlin/supplier/EntitySupplier.kt +++ b/core/src/main/kotlin/supplier/EntitySupplier.kt @@ -12,6 +12,7 @@ import dev.kord.core.entity.channel.MessageChannel import dev.kord.core.entity.channel.TopGuildChannel import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.entity.channel.thread.ThreadMember +import dev.kord.core.entity.interaction.PublicFollowupMessage import dev.kord.core.exception.EntityNotFoundException import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant @@ -392,6 +393,38 @@ public interface EntitySupplier { public suspend fun getWebhookWithToken(id: Snowflake, token: String): Webhook = getWebhookWithTokenOrNull(id, token) ?: EntityNotFoundException.webhookNotFound(id) + /** + * Requests the [Message] with the given [messageId] previously sent from a [Webhook] with the given [webhookId] + * using the [token] for authentication, returns `null` when the message isn't present. + * + * If the message is in a thread, [threadId] must be specified. + * + * @throws RequestException if something went wrong while retrieving the message. + */ + public suspend fun getWebhookMessageOrNull( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake? = null, + ): Message? + + /** + * Requests the [Message] with the given [messageId] previously sent from a [Webhook] with the given [webhookId] + * using the [token] for authentication. + * + * If the message is in a thread, [threadId] must be specified. + * + * @throws RequestException if something went wrong while retrieving the message. + * @throws EntityNotFoundException if the message is null. + */ + public suspend fun getWebhookMessage( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake? = null, + ): Message = getWebhookMessageOrNull(webhookId, token, messageId, threadId) + ?: EntityNotFoundException.webhookMessageNotFound(webhookId, token, messageId, threadId) + /** * Requests the [Template] with the given [code]. * returns null when the template isn't present. @@ -493,6 +526,32 @@ public interface EntitySupplier { guildId: Snowflake, ): Flow + /** + * Requests a followup message for an interaction response. Does not support ephemeral followups. + * Returns `null` if the followup message isn't present. + * + * @throws RequestException if something went wrong during the request. + */ + public suspend fun getFollowupMessageOrNull( + applicationId: Snowflake, + interactionToken: String, + messageId: Snowflake, + ): PublicFollowupMessage? + + /** + * Requests a followup message for an interaction response. Does not support ephemeral followups. + * + * @throws RequestException if something went wrong during the request. + * @throws EntityNotFoundException if the followup message is null. + */ + public suspend fun getFollowupMessage( + applicationId: Snowflake, + interactionToken: String, + messageId: Snowflake, + ): PublicFollowupMessage = + getFollowupMessageOrNull(applicationId, interactionToken, messageId) + ?: EntityNotFoundException.followupMessageNotFound(interactionToken, messageId) + public fun getGuildScheduledEvents(guildId: Snowflake): Flow public suspend fun getGuildScheduledEventOrNull(guildId: Snowflake, eventId: Snowflake): GuildScheduledEvent? diff --git a/core/src/main/kotlin/supplier/FallbackEntitySupplier.kt b/core/src/main/kotlin/supplier/FallbackEntitySupplier.kt index 617323a72971..0c09536718dc 100644 --- a/core/src/main/kotlin/supplier/FallbackEntitySupplier.kt +++ b/core/src/main/kotlin/supplier/FallbackEntitySupplier.kt @@ -9,6 +9,7 @@ import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.TopGuildChannel import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.entity.channel.thread.ThreadMember +import dev.kord.core.entity.interaction.PublicFollowupMessage import dev.kord.core.switchIfEmpty import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant @@ -105,6 +106,14 @@ private class FallbackEntitySupplier(val first: EntitySupplier, val second: Enti override suspend fun getWebhookWithTokenOrNull(id: Snowflake, token: String): Webhook? = first.getWebhookWithTokenOrNull(id, token) ?: second.getWebhookWithTokenOrNull(id, token) + override suspend fun getWebhookMessageOrNull( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake?, + ): Message? = first.getWebhookMessageOrNull(webhookId, token, messageId, threadId) + ?: second.getWebhookMessageOrNull(webhookId, token, messageId, threadId) + override suspend fun getGuildPreviewOrNull(guildId: Snowflake): GuildPreview? = first.getGuildPreviewOrNull(guildId) ?: second.getGuildPreviewOrNull(guildId) @@ -195,6 +204,13 @@ private class FallbackEntitySupplier(val first: EntitySupplier, val second: Enti first.getGuildApplicationCommandPermissions(applicationId, guildId) .switchIfEmpty(second.getGuildApplicationCommandPermissions(applicationId, guildId)) + override suspend fun getFollowupMessageOrNull( + applicationId: Snowflake, + interactionToken: String, + messageId: Snowflake, + ): PublicFollowupMessage? = first.getFollowupMessageOrNull(applicationId, interactionToken, messageId) + ?: second.getFollowupMessageOrNull(applicationId, interactionToken, messageId) + override fun getGuildScheduledEvents(guildId: Snowflake): Flow = first.getGuildScheduledEvents(guildId).switchIfEmpty(second.getGuildScheduledEvents(guildId)) diff --git a/core/src/main/kotlin/supplier/RestEntitySupplier.kt b/core/src/main/kotlin/supplier/RestEntitySupplier.kt index 5b9f4d14367b..b3ff4cb90cd8 100644 --- a/core/src/main/kotlin/supplier/RestEntitySupplier.kt +++ b/core/src/main/kotlin/supplier/RestEntitySupplier.kt @@ -14,6 +14,7 @@ import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.TopGuildChannel import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.entity.channel.thread.ThreadMember +import dev.kord.core.entity.interaction.PublicFollowupMessage import dev.kord.core.exception.EntityNotFoundException import dev.kord.rest.builder.auditlog.AuditLogGetRequestBuilder import dev.kord.rest.json.request.AuditLogGetRequest @@ -240,6 +241,17 @@ public class RestEntitySupplier(public val kord: Kord) : EntitySupplier { Webhook(data, kord) } + override suspend fun getWebhookMessageOrNull( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake?, + ): Message? = catchNotFound { + val response = webhook.getWebhookMessage(webhookId, token, messageId, threadId) + val data = MessageData.from(response) + Message(data, kord) + } + public suspend fun getInviteOrNull(code: String, withCounts: Boolean): Invite? = catchNotFound { val response = invite.getInvite(code, withCounts) Invite(InviteData.from(response), kord) @@ -393,15 +405,35 @@ public class RestEntitySupplier(public val kord: Kord) : EntitySupplier { } } + /** + * Requests the initial interaction response, returns `null` if the initial interaction response isn't present. + * + * @throws RestRequestException if something went wrong during the request. + */ public suspend fun getOriginalInteractionOrNull(applicationId: Snowflake, token: String): Message? = catchNotFound { val response = interaction.getInteractionResponse(applicationId, token) val data = MessageData.from(response) Message(data, kord) } + /** + * Requests the initial interaction response. + * + * @throws RestRequestException if something went wrong during the request. + * @throws EntityNotFoundException if the initial interaction response is null. + */ + public suspend fun getOriginalInteraction(applicationId: Snowflake, token: String): Message = + getOriginalInteractionOrNull(applicationId, token) ?: EntityNotFoundException.interactionNotFound(token) - public suspend fun getOriginalInteraction(applicationId: Snowflake, token: String): Message { - return getOriginalInteractionOrNull(applicationId, token) ?: EntityNotFoundException.interactionNotFound(token) + override suspend fun getFollowupMessageOrNull( + applicationId: Snowflake, + interactionToken: String, + messageId: Snowflake, + ): PublicFollowupMessage? = catchNotFound { + val response = interaction.getFollowupMessage(applicationId, interactionToken, messageId) + val data = MessageData.from(response) + val message = Message(data, kord) + PublicFollowupMessage(message, applicationId, interactionToken, kord) } override fun getGuildApplicationCommandPermissions( diff --git a/core/src/main/kotlin/supplier/StoreEntitySupplier.kt b/core/src/main/kotlin/supplier/StoreEntitySupplier.kt index f82ccb637362..6a2e2c593c22 100644 --- a/core/src/main/kotlin/supplier/StoreEntitySupplier.kt +++ b/core/src/main/kotlin/supplier/StoreEntitySupplier.kt @@ -12,6 +12,7 @@ import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.TopGuildChannel import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.entity.channel.thread.ThreadMember +import dev.kord.core.entity.interaction.PublicFollowupMessage import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.onEach import kotlinx.datetime.Instant @@ -149,6 +150,15 @@ public class StoreEntitySupplier( return storeAndReturn(supplier.getWebhookWithTokenOrNull(id, token)) { it.data } } + override suspend fun getWebhookMessageOrNull( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake?, + ): Message? { + return storeAndReturn(supplier.getWebhookMessageOrNull(webhookId, token, messageId, threadId)) { it.data } + } + override suspend fun getTemplateOrNull(code: String): Template? { return storeAndReturn(supplier.getTemplateOrNull(code)) { it.data } } @@ -225,6 +235,16 @@ public class StoreEntitySupplier( ) { it.data } } + override suspend fun getFollowupMessageOrNull( + applicationId: Snowflake, + interactionToken: String, + messageId: Snowflake, + ): PublicFollowupMessage? { + return storeAndReturn(supplier.getFollowupMessageOrNull(applicationId, interactionToken, messageId)) { + it.message.data + } + } + override fun getGuildScheduledEvents(guildId: Snowflake): Flow = storeOnEach(supplier.getGuildScheduledEvents(guildId)) { it.data } diff --git a/core/src/test/kotlin/rest/RestTest.kt b/core/src/test/kotlin/rest/RestTest.kt index 2c688bb3fc5f..a98c209c5ce2 100644 --- a/core/src/test/kotlin/rest/RestTest.kt +++ b/core/src/test/kotlin/rest/RestTest.kt @@ -377,7 +377,7 @@ class RestServiceTest { content = "a" } - message.editWebhookMessage(webhook.id, webhook.token!!) { + message.edit(webhook.id, webhook.token!!) { content = "b" } diff --git a/rest/src/main/kotlin/route/Route.kt b/rest/src/main/kotlin/route/Route.kt index 62a21bf055ff..75adb2974cc2 100644 --- a/rest/src/main/kotlin/route/Route.kt +++ b/rest/src/main/kotlin/route/Route.kt @@ -79,7 +79,7 @@ public sealed class Route( public object TemplateCode : Key("{template.code}") public object ApplicationId : Key("{application.id}", true) public object CommandId : Key("{command.id}", true) - public object InteractionId : Key("interaction.id", true) + public object InteractionId : Key("{interaction.id}", true) public object InteractionToken : Key("{interaction.token}", true) public object ScheduledEventId : Key("{event.id}", true) public object StickerId : Key("{sticker.id}") @@ -621,6 +621,14 @@ public sealed class Route( requiresAuthorizationHeader = false, ) + public object GetWebhookMessage : + Route( + HttpMethod.Get, + "/webhooks/$WebhookId/$WebhookToken/messages/$MessageId", + DiscordMessage.serializer(), + requiresAuthorizationHeader = false, + ) + public object EditWebhookMessage : Route( HttpMethod.Patch, @@ -629,6 +637,14 @@ public sealed class Route( requiresAuthorizationHeader = false, ) + public object DeleteWebhookMessage : + Route( + HttpMethod.Delete, + "/webhooks/$WebhookId/$WebhookToken/messages/$MessageId", + NoStrategy, + requiresAuthorizationHeader = false, + ) + /* * Voice: @@ -890,6 +906,14 @@ public sealed class Route( requiresAuthorizationHeader = false, ) + public object FollowupMessageGet : + Route( + HttpMethod.Get, + "/webhooks/$ApplicationId/$InteractionToken/messages/$MessageId", + DiscordMessage.serializer(), + requiresAuthorizationHeader = false, + ) + public object FollowupMessageModify : Route( HttpMethod.Patch, diff --git a/rest/src/main/kotlin/service/InteractionService.kt b/rest/src/main/kotlin/service/InteractionService.kt index 5a0a38b53555..9a41ac05a483 100644 --- a/rest/src/main/kotlin/service/InteractionService.kt +++ b/rest/src/main/kotlin/service/InteractionService.kt @@ -1,6 +1,7 @@ package dev.kord.rest.service import dev.kord.common.entity.* +import dev.kord.common.entity.MessageFlag.Ephemeral import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.coerceToMissing import dev.kord.common.entity.optional.orEmpty @@ -10,6 +11,7 @@ import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder import dev.kord.rest.builder.message.modify.FollowupMessageModifyBuilder import dev.kord.rest.builder.message.modify.InteractionResponseModifyBuilder import dev.kord.rest.json.request.* +import dev.kord.rest.request.RequestBuilder import dev.kord.rest.request.RequestHandler import dev.kord.rest.route.Route import kotlinx.serialization.KSerializer @@ -18,7 +20,6 @@ import kotlinx.serialization.serializer import kotlin.contracts.InvocationKind import kotlin.contracts.contract - public class InteractionService(requestHandler: RequestHandler) : RestService(requestHandler) { public suspend fun getGlobalApplicationCommands(applicationId: Snowflake): List = @@ -47,23 +48,20 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re commandId: Snowflake, request: ApplicationCommandModifyRequest, ): DiscordApplicationCommand = call(Route.GlobalApplicationCommandModify) { - keys[Route.ApplicationId] = applicationId - keys[Route.CommandId] = commandId + applicationIdCommandId(applicationId, commandId) body(ApplicationCommandModifyRequest.serializer(), request) } public suspend fun deleteGlobalApplicationCommand(applicationId: Snowflake, commandId: Snowflake): Unit = call(Route.GlobalApplicationCommandDelete) { - keys[Route.ApplicationId] = applicationId - keys[Route.CommandId] = commandId + applicationIdCommandId(applicationId, commandId) } public suspend fun getGuildApplicationCommands( applicationId: Snowflake, guildId: Snowflake, ): List = call(Route.GuildApplicationCommandsGet) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId + applicationIdGuildId(applicationId, guildId) } public suspend fun createGuildApplicationCommand( @@ -71,8 +69,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re guildId: Snowflake, request: ApplicationCommandCreateRequest, ): DiscordApplicationCommand = call(Route.GuildApplicationCommandCreate) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId + applicationIdGuildId(applicationId, guildId) body(ApplicationCommandCreateRequest.serializer(), request) } @@ -81,8 +78,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re guildId: Snowflake, request: List, ): List = call(Route.GuildApplicationCommandsCreate) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId + applicationIdGuildId(applicationId, guildId) body(ListSerializer(ApplicationCommandCreateRequest.serializer()), request) } @@ -92,9 +88,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re commandId: Snowflake, request: ApplicationCommandModifyRequest, ): DiscordApplicationCommand = call(Route.GuildApplicationCommandModify) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId - keys[Route.CommandId] = commandId + applicationIdGuildIdCommandId(applicationId, guildId, commandId) body(ApplicationCommandModifyRequest.serializer(), request) } @@ -103,9 +97,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re guildId: Snowflake, commandId: Snowflake, ): Unit = call(Route.GuildApplicationCommandDelete) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId - keys[Route.CommandId] = commandId + applicationIdGuildIdCommandId(applicationId, guildId, commandId) } public suspend fun createInteractionResponse( @@ -113,8 +105,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re interactionToken: String, request: MultipartInteractionResponseCreateRequest, ): Unit = call(Route.InteractionResponseCreate) { - keys[Route.InteractionId] = interactionId - keys[Route.InteractionToken] = interactionToken + interactionIdInteractionToken(interactionId, interactionToken) body(InteractionResponseCreateRequest.serializer(), request.request) request.files.orEmpty().onEach { file(it) } } @@ -124,8 +115,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re interactionToken: String, request: InteractionResponseCreateRequest, ): Unit = call(Route.InteractionResponseCreate) { - keys[Route.InteractionId] = interactionId - keys[Route.InteractionToken] = interactionToken + interactionIdInteractionToken(interactionId, interactionToken) body(InteractionResponseCreateRequest.serializer(), request) } @@ -135,9 +125,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re autoComplete: DiscordAutoComplete, typeSerializer: KSerializer = serializer(), ): Unit = call(Route.InteractionResponseCreate) { - keys[Route.InteractionId] = interactionId - keys[Route.InteractionToken] = interactionToken - + interactionIdInteractionToken(interactionId, interactionToken) body( AutoCompleteResponseCreateRequest.serializer(typeSerializer), AutoCompleteResponseCreateRequest( @@ -212,8 +200,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re public suspend fun getInteractionResponse(applicationId: Snowflake, interactionToken: String): DiscordMessage = call(Route.OriginalInteractionResponseGet) { - keys[Route.ApplicationId] = applicationId - keys[Route.InteractionToken] = interactionToken + applicationIdInteractionToken(applicationId, interactionToken) } public suspend fun modifyInteractionResponse( @@ -221,8 +208,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re interactionToken: String, multipartRequest: MultipartInteractionResponseModifyRequest, ): DiscordMessage = call(Route.OriginalInteractionResponseModify) { - keys[Route.ApplicationId] = applicationId - keys[Route.InteractionToken] = interactionToken + applicationIdInteractionToken(applicationId, interactionToken) body(InteractionResponseModifyRequest.serializer(), multipartRequest.request) multipartRequest.files.orEmpty().forEach { file(it) } } @@ -232,15 +218,13 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re interactionToken: String, request: InteractionResponseModifyRequest, ): DiscordMessage = call(Route.OriginalInteractionResponseModify) { - keys[Route.ApplicationId] = applicationId - keys[Route.InteractionToken] = interactionToken + applicationIdInteractionToken(applicationId, interactionToken) body(InteractionResponseModifyRequest.serializer(), request) } public suspend fun deleteOriginalInteractionResponse(applicationId: Snowflake, interactionToken: String): Unit = call(Route.OriginalInteractionResponseDelete) { - keys[Route.ApplicationId] = applicationId - keys[Route.InteractionToken] = interactionToken + applicationIdInteractionToken(applicationId, interactionToken) } public suspend fun createFollowupMessage( @@ -248,20 +232,25 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re interactionToken: String, multipart: MultipartFollowupMessageCreateRequest, ): DiscordMessage = call(Route.FollowupMessageCreate) { - keys[Route.ApplicationId] = applicationId - keys[Route.InteractionToken] = interactionToken + applicationIdInteractionToken(applicationId, interactionToken) body(FollowupMessageCreateRequest.serializer(), multipart.request) multipart.files.forEach { file(it) } } + public suspend fun getFollowupMessage( + applicationId: Snowflake, + interactionToken: String, + messageId: Snowflake, + ): DiscordMessage = call(Route.FollowupMessageGet) { + applicationIdInteractionTokenMessageId(applicationId, interactionToken, messageId) + } + public suspend fun deleteFollowupMessage( applicationId: Snowflake, interactionToken: String, messageId: Snowflake, ): Unit = call(Route.FollowupMessageDelete) { - keys[Route.ApplicationId] = applicationId - keys[Route.InteractionToken] = interactionToken - keys[Route.MessageId] = messageId + applicationIdInteractionTokenMessageId(applicationId, interactionToken, messageId) } public suspend fun modifyFollowupMessage( @@ -270,9 +259,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re messageId: Snowflake, request: MultipartFollowupMessageModifyRequest, ): DiscordMessage = call(Route.FollowupMessageModify) { - keys[Route.ApplicationId] = applicationId - keys[Route.InteractionToken] = interactionToken - keys[Route.MessageId] = messageId + applicationIdInteractionTokenMessageId(applicationId, interactionToken, messageId) body(FollowupMessageModifyRequest.serializer(), request.request) request.files.orEmpty().forEach { file(it) } } @@ -283,16 +270,13 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re messageId: Snowflake, request: FollowupMessageModifyRequest, ): DiscordMessage = call(Route.FollowupMessageModify) { - keys[Route.ApplicationId] = applicationId - keys[Route.InteractionToken] = interactionToken - keys[Route.MessageId] = messageId + applicationIdInteractionTokenMessageId(applicationId, interactionToken, messageId) body(FollowupMessageModifyRequest.serializer(), request) } public suspend fun getGlobalCommand(applicationId: Snowflake, commandId: Snowflake): DiscordApplicationCommand = call(Route.GlobalApplicationCommandGet) { - keys[Route.ApplicationId] = applicationId - keys[Route.CommandId] = commandId + applicationIdCommandId(applicationId, commandId) } public suspend fun getGuildCommand( @@ -300,17 +284,14 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re guildId: Snowflake, commandId: Snowflake, ): DiscordApplicationCommand = call(Route.GuildApplicationCommandGet) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId - keys[Route.CommandId] = commandId + applicationIdGuildIdCommandId(applicationId, guildId, commandId) } public suspend fun getGuildApplicationCommandPermissions( applicationId: Snowflake, guildId: Snowflake, ): List = call(Route.GuildApplicationCommandPermissionsGet) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId + applicationIdGuildId(applicationId, guildId) } public suspend fun getApplicationCommandPermissions( @@ -318,9 +299,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re guildId: Snowflake, commandId: Snowflake, ): DiscordGuildApplicationCommandPermissions = call(Route.ApplicationCommandPermissionsGet) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId - keys[Route.CommandId] = commandId + applicationIdGuildIdCommandId(applicationId, guildId, commandId) } public suspend fun editApplicationCommandPermissions( @@ -329,10 +308,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re commandId: Snowflake, request: ApplicationCommandPermissionsEditRequest, ): DiscordGuildApplicationCommandPermissions = call(Route.ApplicationCommandPermissionsPut) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId - keys[Route.CommandId] = commandId - + applicationIdGuildIdCommandId(applicationId, guildId, commandId) body(ApplicationCommandPermissionsEditRequest.serializer(), request) } @@ -341,9 +317,7 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re guildId: Snowflake, request: List, ): List = call(Route.ApplicationCommandPermissionsBatchPut) { - keys[Route.ApplicationId] = applicationId - keys[Route.GuildId] = guildId - + applicationIdGuildId(applicationId, guildId) body(ListSerializer(PartialDiscordGuildApplicationCommandPermissions.serializer()), request) } @@ -551,12 +525,29 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re ) } + + @Deprecated( + "'createPublicInteractionResponse' was renamed to 'createInteractionResponse'", + ReplaceWith("this.createInteractionResponse(interactionId, interactionToken, ephemeral, builder)"), + DeprecationLevel.ERROR, + ) public suspend inline fun createPublicInteractionResponse( interactionId: Snowflake, interactionToken: String, ephemeral: Boolean = false, builder: InteractionResponseCreateBuilder.() -> Unit ) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } + return createInteractionResponse(interactionId, interactionToken, ephemeral, builder) + } + + public suspend inline fun createInteractionResponse( + interactionId: Snowflake, + interactionToken: String, + ephemeral: Boolean = false, + builder: InteractionResponseCreateBuilder.() -> Unit, + ) { + contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } return createInteractionResponse( interactionId, interactionToken, @@ -635,14 +626,52 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re } public suspend fun acknowledge(interactionId: Snowflake, interactionToken: String, ephemeral: Boolean = false) { + val flags = if (ephemeral) MessageFlags(Ephemeral) else null val request = InteractionResponseCreateRequest( type = InteractionResponseType.DeferredChannelMessageWithSource, data = Optional( - InteractionApplicationCommandCallbackData( - flags = Optional(if (ephemeral) MessageFlags(MessageFlag.Ephemeral) else null).coerceToMissing() - ) - ) + flags?.let { InteractionApplicationCommandCallbackData(flags = Optional(it)) } + ).coerceToMissing() ) createInteractionResponse(interactionId, interactionToken, request) } } + +private fun RequestBuilder<*>.applicationIdCommandId(applicationId: Snowflake, commandId: Snowflake) { + keys[Route.ApplicationId] = applicationId + keys[Route.CommandId] = commandId +} + +private fun RequestBuilder<*>.applicationIdGuildId(applicationId: Snowflake, guildId: Snowflake) { + keys[Route.ApplicationId] = applicationId + keys[Route.GuildId] = guildId +} + +private fun RequestBuilder<*>.applicationIdGuildIdCommandId( + applicationId: Snowflake, + guildId: Snowflake, + commandId: Snowflake, +) { + applicationIdGuildId(applicationId, guildId) + keys[Route.CommandId] = commandId +} + +@PublishedApi +internal fun RequestBuilder<*>.interactionIdInteractionToken(interactionId: Snowflake, interactionToken: String) { + keys[Route.InteractionId] = interactionId + keys[Route.InteractionToken] = interactionToken +} + +private fun RequestBuilder<*>.applicationIdInteractionToken(applicationId: Snowflake, interactionToken: String) { + keys[Route.ApplicationId] = applicationId + keys[Route.InteractionToken] = interactionToken +} + +private fun RequestBuilder<*>.applicationIdInteractionTokenMessageId( + applicationId: Snowflake, + interactionToken: String, + messageId: Snowflake, +) { + applicationIdInteractionToken(applicationId, interactionToken) + keys[Route.MessageId] = messageId +} diff --git a/rest/src/main/kotlin/service/WebhookService.kt b/rest/src/main/kotlin/service/WebhookService.kt index fd1710ca7dbc..70736c0e97d6 100644 --- a/rest/src/main/kotlin/service/WebhookService.kt +++ b/rest/src/main/kotlin/service/WebhookService.kt @@ -13,6 +13,7 @@ import dev.kord.rest.json.request.WebhookCreateRequest import dev.kord.rest.json.request.WebhookEditMessageRequest import dev.kord.rest.json.request.WebhookExecuteRequest import dev.kord.rest.json.request.WebhookModifyRequest +import dev.kord.rest.request.RequestBuilder import dev.kord.rest.request.RequestHandler import dev.kord.rest.request.auditLogReason import dev.kord.rest.route.Route @@ -53,8 +54,7 @@ public class WebhookService(requestHandler: RequestHandler) : RestService(reques public suspend fun getWebhookWithToken(webhookId: Snowflake, token: String): DiscordWebhook = call(Route.WebhookByTokenGet) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token + webhookIdToken(webhookId, token) } public suspend inline fun modifyWebhook( @@ -83,8 +83,7 @@ public class WebhookService(requestHandler: RequestHandler) : RestService(reques } return call(Route.WebhookByTokenPatch) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token + webhookIdToken(webhookId, token) val modifyBuilder = WebhookModifyBuilder().apply(builder) body(WebhookModifyRequest.serializer(), modifyBuilder.toRequest()) auditLogReason(modifyBuilder.reason) @@ -98,8 +97,7 @@ public class WebhookService(requestHandler: RequestHandler) : RestService(reques public suspend fun deleteWebhookWithToken(webhookId: Snowflake, token: String, reason: String? = null): Unit = call(Route.WebhookByTokenDelete) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token + webhookIdToken(webhookId, token) auditLogReason(reason) } @@ -115,10 +113,7 @@ public class WebhookService(requestHandler: RequestHandler) : RestService(reques } return call(Route.ExecuteWebhookPost) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token - if (wait != null) parameter("wait", "$wait") - if (threadId != null) parameter("thread_id", threadId.toString()) + webhookIdTokenWaitThreadId(webhookId, token, wait, threadId) val request = WebhookMessageCreateBuilder().apply(builder).toRequest() body(WebhookExecuteRequest.serializer(), request.request) request.files.forEach { file(it) } @@ -130,11 +125,10 @@ public class WebhookService(requestHandler: RequestHandler) : RestService(reques webhookId: Snowflake, token: String, body: JsonObject, - wait: Boolean = false, + wait: Boolean? = null, + threadId: Snowflake? = null, ): Unit = call(Route.ExecuteSlackWebhookPost) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token - parameter("wait", "$wait") + webhookIdTokenWaitThreadId(webhookId, token, wait, threadId) body(JsonObject.serializer(), body) } @@ -143,18 +137,27 @@ public class WebhookService(requestHandler: RequestHandler) : RestService(reques webhookId: Snowflake, token: String, body: JsonObject, - wait: Boolean = false, + wait: Boolean? = null, + threadId: Snowflake? = null, ): Unit = call(Route.ExecuteGithubWebhookPost) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token - parameter("wait", "$wait") + webhookIdTokenWaitThreadId(webhookId, token, wait, threadId) body(JsonObject.serializer(), body) } + public suspend fun getWebhookMessage( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake? = null, + ): DiscordMessage = call(Route.GetWebhookMessage) { + webhookIdTokenMessageIdThreadId(webhookId, token, messageId, threadId) + } + public suspend inline fun editWebhookMessage( webhookId: Snowflake, token: String, messageId: Snowflake, + threadId: Snowflake? = null, builder: WebhookMessageModifyBuilder.() -> Unit ): DiscordMessage { contract { @@ -162,13 +165,49 @@ public class WebhookService(requestHandler: RequestHandler) : RestService(reques } return call(Route.EditWebhookMessage) { - - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token - keys[Route.MessageId] = messageId + webhookIdTokenMessageIdThreadId(webhookId, token, messageId, threadId) val body = WebhookMessageModifyBuilder().apply(builder).toRequest() body(WebhookEditMessageRequest.serializer(), body.request) body.files.orEmpty().onEach { file(it) } } } + + public suspend fun deleteWebhookMessage( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake? = null, + ): Unit = call(Route.DeleteWebhookMessage) { + webhookIdTokenMessageIdThreadId(webhookId, token, messageId, threadId) + } +} + +@PublishedApi +internal fun RequestBuilder<*>.webhookIdToken(webhookId: Snowflake, token: String) { + keys[Route.WebhookId] = webhookId + keys[Route.WebhookToken] = token +} + +@PublishedApi +internal fun RequestBuilder<*>.webhookIdTokenWaitThreadId( + webhookId: Snowflake, + token: String, + wait: Boolean?, + threadId: Snowflake?, +) { + webhookIdToken(webhookId, token) + wait?.let { parameter("wait", it) } + threadId?.let { parameter("thread_id", it) } +} + +@PublishedApi +internal fun RequestBuilder<*>.webhookIdTokenMessageIdThreadId( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + threadId: Snowflake?, +) { + webhookIdToken(webhookId, token) + keys[Route.MessageId] = messageId + threadId?.let { parameter("thread_id", it) } }