From 847bba8ad811d61c551f35acac0a5ab03df4bda7 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Thu, 25 Mar 2021 20:34:40 +0100 Subject: [PATCH] Add New webhook endpoints (discord/discord-api-docs#2243) Relates to #121 --- .../main/kotlin/behavior/MessageBehavior.kt | 81 ++++- core/src/main/kotlin/entity/Message.kt | 43 ++- .../builder/message/MessageModifyBuilder.kt | 13 +- .../webhook/EditWebhookMessageBuilder.kt | 35 +++ .../kotlin/json/request/MessageRequests.kt | 2 +- .../kotlin/json/request/WebhookRequests.kt | 35 ++- rest/src/main/kotlin/route/Route.kt | 293 ++++++++++++++---- .../src/main/kotlin/service/WebhookService.kt | 109 +++++-- 8 files changed, 489 insertions(+), 122 deletions(-) create mode 100644 rest/src/main/kotlin/builder/webhook/EditWebhookMessageBuilder.kt diff --git a/core/src/main/kotlin/behavior/MessageBehavior.kt b/core/src/main/kotlin/behavior/MessageBehavior.kt index 95f6b5ed32f..ddf284c3f0c 100644 --- a/core/src/main/kotlin/behavior/MessageBehavior.kt +++ b/core/src/main/kotlin/behavior/MessageBehavior.kt @@ -17,6 +17,7 @@ import dev.kord.core.supplier.getChannelOf import dev.kord.core.supplier.getChannelOfOrNull import dev.kord.rest.builder.message.MessageCreateBuilder import dev.kord.rest.builder.message.MessageModifyBuilder +import dev.kord.rest.builder.webhook.EditWebhookMessageBuilder import dev.kord.rest.request.RestRequestException import dev.kord.rest.service.RestClient import kotlinx.coroutines.flow.Flow @@ -39,6 +40,8 @@ interface MessageBehavior : KordEntity, Strategizable { */ val channel get() = MessageChannelBehavior(channelId, kord) + val webhookId: Snowflake? + /** * Requests to get the channel this message was send in. */ @@ -60,7 +63,8 @@ interface MessageBehavior : KordEntity, Strategizable { * * @throws [RequestException] if anything went wrong during the request. */ - suspend fun asMessageOrNull(): Message? = supplier.getMessageOrNull(channelId = channelId, messageId = id) + suspend fun asMessageOrNull(): Message? = + supplier.getMessageOrNull(channelId = channelId, messageId = id) /** @@ -82,7 +86,7 @@ interface MessageBehavior : KordEntity, Strategizable { */ fun getReactors(emoji: ReactionEmoji): Flow = - kord.with(EntitySupplyStrategy.rest).getReactors(channelId, id, emoji) + kord.with(EntitySupplyStrategy.rest).getReactors(channelId, id, emoji) /** * Requests to add an [emoji] to this message. @@ -90,7 +94,11 @@ interface MessageBehavior : KordEntity, Strategizable { * @throws [RestRequestException] if something went wrong during the request. */ suspend fun addReaction(emoji: ReactionEmoji) { - kord.rest.channel.createReaction(channelId = channelId, messageId = id, emoji = emoji.urlFormat) + kord.rest.channel.createReaction( + channelId = channelId, + messageId = id, + emoji = emoji.urlFormat + ) } /** @@ -126,7 +134,12 @@ interface MessageBehavior : KordEntity, Strategizable { * @throws [RestRequestException] if something went wrong during the request. */ suspend fun deleteReaction(userId: Snowflake, emoji: ReactionEmoji) { - kord.rest.channel.deleteReaction(channelId = channelId, messageId = id, userId = userId, emoji = emoji.urlFormat) + kord.rest.channel.deleteReaction( + channelId = channelId, + messageId = id, + userId = userId, + emoji = emoji.urlFormat + ) } /** @@ -135,7 +148,11 @@ interface MessageBehavior : KordEntity, Strategizable { * @throws [RestRequestException] if something went wrong during the request. */ suspend fun deleteOwnReaction(emoji: ReactionEmoji) { - kord.rest.channel.deleteOwnReaction(channelId = channelId, messageId = id, emoji = emoji.urlFormat) + kord.rest.channel.deleteOwnReaction( + channelId = channelId, + messageId = id, + emoji = emoji.urlFormat + ) } /** @@ -153,7 +170,11 @@ interface MessageBehavior : KordEntity, Strategizable { * @throws [RestRequestException] if something went wrong during the request. */ suspend fun deleteReaction(emoji: ReactionEmoji) { - kord.rest.channel.deleteAllReactionsForEmoji(channelId = channelId, messageId = id, emoji = emoji.urlFormat) + kord.rest.channel.deleteAllReactionsForEmoji( + channelId = channelId, + messageId = id, + emoji = emoji.urlFormat + ) } /** @@ -178,20 +199,22 @@ interface MessageBehavior : KordEntity, Strategizable { * Returns a new [MessageBehavior] with the given [strategy]. */ override fun withStrategy( - strategy: EntitySupplyStrategy<*>, - ): MessageBehavior = MessageBehavior(channelId, id, kord, strategy) + strategy: EntitySupplyStrategy<*>, + ): MessageBehavior = MessageBehavior(channelId, id, kord, webhookId, strategy) } - fun MessageBehavior( +fun MessageBehavior( channelId: Snowflake, messageId: Snowflake, kord: Kord, + webhookId: Snowflake? = null, strategy: EntitySupplyStrategy<*> = kord.resources.defaultStrategy, ) = object : MessageBehavior { override val channelId: Snowflake = channelId override val id: Snowflake = messageId override val kord: Kord = kord + override val webhookId: Snowflake? = webhookId override val supplier: EntitySupplier = strategy.supply(kord) override fun hashCode(): Int = Objects.hash(id) @@ -212,13 +235,51 @@ interface MessageBehavior : KordEntity, Strategizable { * @return The edited [Message]. * * @throws [RestRequestException] if something went wrong during the request. + * @throws [IllegalStateException] if this message is a webhook message (See [editWebhookMessage]). + * @see editWebhookMessage */ @OptIn(ExperimentalContracts::class) suspend inline fun MessageBehavior.edit(builder: MessageModifyBuilder.() -> Unit): Message { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } - val response = kord.rest.channel.editMessage(channelId = channelId, messageId = id, builder = builder) + + require(webhookId == null) { "Cannot perform edits on webhook messages use editWebhook() instead" } + + val response = + kord.rest.channel.editMessage(channelId = channelId, messageId = id, builder = builder) + val data = MessageData.from(response) + + return Message(data, kord) +} + +/** + * Requests to edit this message. + * + * @return The edited [Message]. + * + * @throws [RestRequestException] if something went wrong during the request. + * @throws [IllegalStateException] if this message is a normal message (see [edit]). + * @see edit + */ +@OptIn(ExperimentalContracts::class) +suspend inline fun MessageBehavior.editWebhookMessage( + token: String, + builder: EditWebhookMessageBuilder.() -> Unit +): Message { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + require(webhookId != null) { "Cannot perform edits on non webhook messages use edit() instead" } + + val response = + kord.rest.webhook.editWebhookMessage( + webhookId = webhookId!!, + messageId = id, + token = token, + builder = builder + ) val data = MessageData.from(response) return Message(data, kord) diff --git a/core/src/main/kotlin/entity/Message.kt b/core/src/main/kotlin/entity/Message.kt index b56f04adbe6..7c1eb9613f9 100644 --- a/core/src/main/kotlin/entity/Message.kt +++ b/core/src/main/kotlin/entity/Message.kt @@ -27,9 +27,9 @@ import java.util.* * An instance of a [Discord Message][https://discord.com/developers/docs/resources/channel#message-object]. */ class Message( - val data: MessageData, - override val kord: Kord, - override val supplier: EntitySupplier = kord.defaultSupplier, + val data: MessageData, + override val kord: Kord, + override val supplier: EntitySupplier = kord.defaultSupplier, ) : MessageBehavior { /** @@ -47,7 +47,8 @@ class Message( /** * The files attached to this message. */ - val attachments: Set get() = data.attachments.asSequence().map { Attachment(it, kord) }.toSet() + val attachments: Set + get() = data.attachments.asSequence().map { Attachment(it, kord) }.toSet() /** * The author of this message, if it was created by a [User]. @@ -86,7 +87,8 @@ class Message( * This collection can only contain values on crossposted messages, channels * mentioned inside the same guild will not be present. */ - val mentionedChannelIds: Set get() = data.mentionedChannels.orEmpty().map { it }.toSet() + val mentionedChannelIds: Set + get() = data.mentionedChannels.orEmpty().map { it }.toSet() /** * The [Channels][ChannelBehavior] specifically mentioned in this message. @@ -94,12 +96,14 @@ class Message( * This collection can only contain values on crossposted messages, channels * mentioned inside the same guild will not be present. */ - val mentionedChannelBehaviors: Set get() = data.mentionedChannels.orEmpty().map { ChannelBehavior(it, kord) }.toSet() + val mentionedChannelBehaviors: Set + get() = data.mentionedChannels.orEmpty().map { ChannelBehavior(it, kord) }.toSet() /** * The stickers sent with this message. */ - val stickers: List get() = data.stickers.orEmpty().map { MessageSticker(it, kord) } + val stickers: List + get() = data.stickers.orEmpty().map { MessageSticker(it, kord) } /** * The message being replied to. @@ -160,7 +164,13 @@ class Message( /** * The [Behaviors][UserBehavior] of users mentioned in this message. */ - val mentionedUserBehaviors: Set get() = data.mentions.map { UserBehavior(it, kord) }.toSet() + val mentionedUserBehaviors: Set + get() = data.mentions.map { + UserBehavior( + it, + kord + ) + }.toSet() /** * The [users][User] mentioned in this message. @@ -182,12 +192,17 @@ class Message( /** * The reactions to this message. */ - val reactions: Set get() = data.reactions.orEmpty().asSequence().map { Reaction(it, kord) }.toSet() + val reactions: Set + get() = data.reactions.orEmpty().asSequence().map { Reaction(it, kord) }.toSet() /** * The instant when this message was created. */ - val timestamp: Instant get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(data.timestamp, Instant::from) + val timestamp: Instant + get() = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse( + data.timestamp, + Instant::from + ) /** * Whether this message was send using `\tts`. @@ -204,7 +219,7 @@ class Message( * * Returns null if this message was not send using a webhook. */ - val webhookId: Snowflake? get() = data.webhookId.value + override val webhookId: Snowflake? get() = data.webhookId.value /** * Returns itself. @@ -237,12 +252,14 @@ class Message( * * @throws [RequestException] if anything went wrong during the request. */ - suspend fun getGuildOrNull(): Guild? = supplier.getChannelOfOrNull(channelId)?.getGuildOrNull() + suspend fun getGuildOrNull(): Guild? = + supplier.getChannelOfOrNull(channelId)?.getGuildOrNull() /** * Returns a new [Message] with the given [strategy]. */ - override fun withStrategy(strategy: EntitySupplyStrategy<*>): Message = Message(data, kord, strategy.supply(kord)) + override fun withStrategy(strategy: EntitySupplyStrategy<*>): Message = + Message(data, kord, strategy.supply(kord)) override fun hashCode(): Int = Objects.hash(id) diff --git a/rest/src/main/kotlin/builder/message/MessageModifyBuilder.kt b/rest/src/main/kotlin/builder/message/MessageModifyBuilder.kt index 43b5e5a1a4f..4203f12657f 100644 --- a/rest/src/main/kotlin/builder/message/MessageModifyBuilder.kt +++ b/rest/src/main/kotlin/builder/message/MessageModifyBuilder.kt @@ -1,10 +1,10 @@ package dev.kord.rest.builder.message -import dev.kord.common.entity.UserFlags import dev.kord.common.annotation.KordDsl +import dev.kord.common.entity.MessageFlags +import dev.kord.common.entity.UserFlags 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.mapNullable import dev.kord.rest.builder.RequestBuilder import dev.kord.rest.json.request.MessageEditPatchRequest @@ -21,8 +21,8 @@ class MessageModifyBuilder : RequestBuilder { private var _embed: Optional = Optional.Missing() var embed: EmbedBuilder? by ::_embed.delegate() - private var _flags: Optional = Optional.Missing() - var flags: UserFlags? by ::_flags.delegate() + private var _flags: Optional = Optional.Missing() + var flags: MessageFlags? by ::_flags.delegate() private var _allowedMentions: Optional = Optional.Missing() var allowedMentions: AllowedMentionsBuilder? by ::_allowedMentions.delegate() @@ -50,6 +50,9 @@ class MessageModifyBuilder : RequestBuilder { override fun toRequest(): MessageEditPatchRequest = MessageEditPatchRequest( - _content, _embed.mapNullable { it?.toRequest() }, _flags, _allowedMentions.mapNullable { it?.build() } + _content, + _embed.mapNullable { it?.toRequest() }, + _flags, + _allowedMentions.mapNullable { it?.build() } ) } diff --git a/rest/src/main/kotlin/builder/webhook/EditWebhookMessageBuilder.kt b/rest/src/main/kotlin/builder/webhook/EditWebhookMessageBuilder.kt new file mode 100644 index 00000000000..e650c8b622e --- /dev/null +++ b/rest/src/main/kotlin/builder/webhook/EditWebhookMessageBuilder.kt @@ -0,0 +1,35 @@ +package dev.kord.rest.builder.webhook + +import dev.kord.common.entity.AllowedMentions +import dev.kord.common.entity.optional.Optional +import dev.kord.common.entity.optional.delegate.delegate +import dev.kord.rest.builder.RequestBuilder +import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.json.request.EmbedRequest +import dev.kord.rest.json.request.WebhookEditMessageRequest +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +class EditWebhookMessageBuilder : RequestBuilder { + + private var _content: Optional = Optional.Missing() + var content: String? by ::_content.delegate() + + var embeds: MutableList = mutableListOf() + + private var _allowedMentions: Optional = Optional.Missing() + var allowedMentions: AllowedMentions? by ::_allowedMentions.delegate() + + @OptIn(ExperimentalContracts::class) + inline fun embed(builder: EmbedBuilder.() -> Unit) { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + embeds.add(EmbedBuilder().apply(builder).toRequest()) + } + + override fun toRequest(): WebhookEditMessageRequest = WebhookEditMessageRequest( + _content, Optional.missingOnEmpty(embeds), _allowedMentions + ) +} \ No newline at end of file diff --git a/rest/src/main/kotlin/json/request/MessageRequests.kt b/rest/src/main/kotlin/json/request/MessageRequests.kt index 51f5317a510..52551fa3fa0 100644 --- a/rest/src/main/kotlin/json/request/MessageRequests.kt +++ b/rest/src/main/kotlin/json/request/MessageRequests.kt @@ -81,7 +81,7 @@ data class EmbedFieldRequest( data class MessageEditPatchRequest( val content: Optional = Optional.Missing(), val embed: Optional = Optional.Missing(), - val flags: Optional = Optional.Missing(), + val flags: Optional = Optional.Missing(), @SerialName("allowed_mentions") val allowedMentions: Optional = Optional.Missing(), ) diff --git a/rest/src/main/kotlin/json/request/WebhookRequests.kt b/rest/src/main/kotlin/json/request/WebhookRequests.kt index ec85c438904..53117036457 100644 --- a/rest/src/main/kotlin/json/request/WebhookRequests.kt +++ b/rest/src/main/kotlin/json/request/WebhookRequests.kt @@ -12,24 +12,31 @@ data class WebhookCreateRequest(val name: String, val avatar: Optional = @Serializable data class WebhookModifyRequest( - val name: Optional = Optional.Missing(), - val avatar: Optional = Optional.Missing(), - @SerialName("channel_id") - val channelId: OptionalSnowflake = OptionalSnowflake.Missing + val name: Optional = Optional.Missing(), + val avatar: Optional = Optional.Missing(), + @SerialName("channel_id") + val channelId: OptionalSnowflake = OptionalSnowflake.Missing ) @Serializable data class WebhookExecuteRequest( - val content: Optional = Optional.Missing(), - val username: Optional = Optional.Missing(), - @SerialName("avatar_url") - val avatar: Optional = Optional.Missing(), - val tts: OptionalBoolean = OptionalBoolean.Missing, - val embeds: Optional> = Optional.Missing(), - val allowedMentions: Optional = Optional.Missing() + val content: Optional = Optional.Missing(), + val username: Optional = Optional.Missing(), + @SerialName("avatar_url") + val avatar: Optional = Optional.Missing(), + val tts: OptionalBoolean = OptionalBoolean.Missing, + val embeds: Optional> = Optional.Missing(), + val allowedMentions: Optional = Optional.Missing() ) data class MultiPartWebhookExecuteRequest( - val request: WebhookExecuteRequest, - val file: Pair? -) \ No newline at end of file + val request: WebhookExecuteRequest, + val file: Pair? +) + +@Serializable +data class WebhookEditMessageRequest( + val content: Optional = Optional.Missing(), + val embeds: Optional> = Optional.Missing(), + val allowedMentions: Optional = Optional.Missing() +) diff --git a/rest/src/main/kotlin/route/Route.kt b/rest/src/main/kotlin/route/Route.kt index 04ab7db7202..6ce32f3dec7 100644 --- a/rest/src/main/kotlin/route/Route.kt +++ b/rest/src/main/kotlin/route/Route.kt @@ -5,7 +5,6 @@ import dev.kord.common.annotation.KordExperimental import dev.kord.common.annotation.KordPreview import dev.kord.common.entity.* import dev.kord.rest.json.optional -import dev.kord.rest.json.request.MessageEditPatchRequest import dev.kord.rest.json.response.* import io.ktor.http.* import kotlinx.serialization.DeserializationStrategy @@ -38,7 +37,11 @@ sealed class Route( : Route(HttpMethod.Get, "/gateway/bot", BotGatewayResponse.serializer()) object AuditLogGet - : Route(HttpMethod.Get, "/guilds/$GuildId/audit-logs", DiscordAuditLog.serializer()) + : Route( + HttpMethod.Get, + "/guilds/$GuildId/audit-logs", + DiscordAuditLog.serializer() + ) object ChannelGet : Route(HttpMethod.Get, "/channels/$ChannelId", DiscordChannel.serializer()) @@ -47,16 +50,29 @@ sealed class Route( : Route(HttpMethod.Put, "/channels/$ChannelId", DiscordChannel.serializer()) object ChannelPatch - : Route(HttpMethod.Patch, "/channels/$ChannelId", DiscordChannel.serializer()) + : + Route(HttpMethod.Patch, "/channels/$ChannelId", DiscordChannel.serializer()) object ChannelDelete - : Route(HttpMethod.Delete, "/channels/$ChannelId", DiscordChannel.serializer()) + : Route( + HttpMethod.Delete, + "/channels/$ChannelId", + DiscordChannel.serializer() + ) object MessagePost - : Route(HttpMethod.Post, "/channels/$ChannelId/messages", DiscordMessage.serializer()) + : Route( + HttpMethod.Post, + "/channels/$ChannelId/messages", + DiscordMessage.serializer() + ) object MessageGet - : Route(HttpMethod.Get, "/channels/$ChannelId/messages/$MessageId", DiscordMessage.serializer()) + : Route( + HttpMethod.Get, + "/channels/$ChannelId/messages/$MessageId", + DiscordMessage.serializer() + ) object MessagesGet : Route>( @@ -80,20 +96,40 @@ sealed class Route( ) object InvitePost - : Route(HttpMethod.Post, "/channels/$ChannelId/invites", DiscordInvite.serializer()) + : Route( + HttpMethod.Post, + "/channels/$ChannelId/invites", + DiscordInvite.serializer() + ) object ReactionPut - : Route(HttpMethod.Put, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/@me", NoStrategy) + : Route( + HttpMethod.Put, + "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/@me", + NoStrategy + ) object OwnReactionDelete - : Route(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/@me", NoStrategy) + : Route( + HttpMethod.Delete, + "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/@me", + NoStrategy + ) object ReactionDelete : - Route(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/$UserId", NoStrategy) + Route( + HttpMethod.Delete, + "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji/$UserId", + NoStrategy + ) object DeleteAllReactionsForEmoji - : Route(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji", NoStrategy) + : Route( + HttpMethod.Delete, + "/channels/$ChannelId/messages/$MessageId/reactions/$Emoji", + NoStrategy + ) object MessageDelete : Route(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId", NoStrategy) @@ -108,10 +144,15 @@ sealed class Route( : Route(HttpMethod.Put, "/channels/$ChannelId/pins/$MessageId", NoStrategy) object AllReactionsDelete - : Route(HttpMethod.Delete, "/channels/$ChannelId/messages/$MessageId/reactions", NoStrategy) + : Route( + HttpMethod.Delete, + "/channels/$ChannelId/messages/$MessageId/reactions", + NoStrategy + ) object ChannelPermissionDelete - : Route(HttpMethod.Delete, "/channels/$ChannelId/permissions/$OverwriteId", NoStrategy) + : + Route(HttpMethod.Delete, "/channels/$ChannelId/permissions/$OverwriteId", NoStrategy) object ChannelPermissionPut : Route(HttpMethod.Put, "/channels/$ChannelId/permissions/$OverwriteId", NoStrategy) @@ -134,13 +175,25 @@ sealed class Route( object EditMessagePatch : - Route(HttpMethod.Patch, "/channels/$ChannelId/messages/$MessageId", DiscordMessage.serializer()) + Route( + HttpMethod.Patch, + "/channels/$ChannelId/messages/$MessageId", + DiscordMessage.serializer() + ) object GuildEmojiGet - : Route(HttpMethod.Get, "/guilds/$GuildId/emojis/$EmojiId", EmojiEntity.serializer()) + : Route( + HttpMethod.Get, + "/guilds/$GuildId/emojis/$EmojiId", + EmojiEntity.serializer() + ) object GuildEmojisGet - : Route>(HttpMethod.Get, "/guilds/$GuildId/emojis", ListSerializer(EmojiEntity.serializer())) + : Route>( + HttpMethod.Get, + "/guilds/$GuildId/emojis", + ListSerializer(EmojiEntity.serializer()) + ) object GuildEmojiDelete : Route(HttpMethod.Delete, "/guilds/$GuildId/emojis/$EmojiId", NoStrategy) @@ -149,13 +202,18 @@ sealed class Route( : Route(HttpMethod.Post, "/guilds/$GuildId/emojis", EmojiEntity.serializer()) object GuildEmojiPatch - : Route(HttpMethod.Patch, "/guilds/$GuildId/emojis/$EmojiId", EmojiEntity.serializer()) + : Route( + HttpMethod.Patch, + "/guilds/$GuildId/emojis/$EmojiId", + EmojiEntity.serializer() + ) object InviteGet : Route(HttpMethod.Get, "/invites/$InviteCode", DiscordInvite.serializer()) object InviteDelete - : Route(HttpMethod.Delete, "/invites/$InviteCode", DiscordInvite.serializer()) + : + Route(HttpMethod.Delete, "/invites/$InviteCode", DiscordInvite.serializer()) object CurrentUserGet : Route(HttpMethod.Get, "/users/@me", DiscordUser.serializer()) @@ -180,7 +238,11 @@ sealed class Route( : Route(HttpMethod.Post, "/users/@me/channels", DiscordChannel.serializer()) object UserConnectionsGet - : Route>(HttpMethod.Get, "/users/@me/connections", ListSerializer(Connection.serializer())) + : Route>( + HttpMethod.Get, + "/users/@me/connections", + ListSerializer(Connection.serializer()) + ) object GuildPost : Route(HttpMethod.Post, "/guilds", DiscordGuild.serializer()) @@ -202,13 +264,21 @@ sealed class Route( ) object GuildChannelsPost - : Route(HttpMethod.Post, "/guilds/$GuildId/channels", DiscordChannel.serializer()) + : Route( + HttpMethod.Post, + "/guilds/$GuildId/channels", + DiscordChannel.serializer() + ) object GuildChannelsPatch : Route(HttpMethod.Patch, "/guilds/$GuildId/channels", NoStrategy) object GuildMemberGet - : Route(HttpMethod.Get, "/guilds/$GuildId/members/$UserId", DiscordGuildMember.serializer()) + : Route( + HttpMethod.Get, + "/guilds/$GuildId/members/$UserId", + DiscordGuildMember.serializer() + ) object GuildMembersGet : Route>( @@ -234,7 +304,11 @@ sealed class Route( object GuildMemberPatch : - Route(HttpMethod.Patch, "/guilds/$GuildId/members/$UserId", DiscordGuildMember.serializer()) + Route( + HttpMethod.Patch, + "/guilds/$GuildId/members/$UserId", + DiscordGuildMember.serializer() + ) object GuildCurrentUserNickPatch : Route( @@ -247,16 +321,25 @@ sealed class Route( : Route(HttpMethod.Put, "/guilds/$GuildId/members/$UserId/roles/$RoleId", NoStrategy) object GuildMemberRoleDelete - : Route(HttpMethod.Delete, "/guilds/$GuildId/members/$UserId/roles/$RoleId", NoStrategy) + : + Route(HttpMethod.Delete, "/guilds/$GuildId/members/$UserId/roles/$RoleId", NoStrategy) object GuildMemberDelete : Route(HttpMethod.Delete, "/guilds/$GuildId/members/$UserId", NoStrategy) object GuildBansGet - : Route>(HttpMethod.Get, "/guilds/$GuildId/bans", ListSerializer(BanResponse.serializer())) + : Route>( + HttpMethod.Get, + "/guilds/$GuildId/bans", + ListSerializer(BanResponse.serializer()) + ) object GuildBanGet - : Route(HttpMethod.Get, "/guilds/$GuildId/bans/$UserId", BanResponse.serializer()) + : Route( + HttpMethod.Get, + "/guilds/$GuildId/bans/$UserId", + BanResponse.serializer() + ) object GuildBanPut : Route(HttpMethod.Put, "/guilds/$GuildId/bans/$UserId", NoStrategy) @@ -265,25 +348,42 @@ sealed class Route( : Route(HttpMethod.Delete, "/guilds/$GuildId/bans/$UserId", NoStrategy) object GuildRolesGet - : Route>(HttpMethod.Get, "/guilds/$GuildId/roles", ListSerializer(DiscordRole.serializer())) + : Route>( + HttpMethod.Get, + "/guilds/$GuildId/roles", + ListSerializer(DiscordRole.serializer()) + ) object GuildRolePost : Route(HttpMethod.Post, "/guilds/$GuildId/roles", DiscordRole.serializer()) object GuildRolesPatch - : Route>(HttpMethod.Patch, "/guilds/$GuildId/roles", ListSerializer(DiscordRole.serializer())) + : Route>( + HttpMethod.Patch, + "/guilds/$GuildId/roles", + ListSerializer(DiscordRole.serializer()) + ) object GuildRolePatch - : Route(HttpMethod.Patch, "/guilds/$GuildId/roles/$RoleId", DiscordRole.serializer()) + : Route( + HttpMethod.Patch, + "/guilds/$GuildId/roles/$RoleId", + DiscordRole.serializer() + ) object GuildRoleDelete : Route(HttpMethod.Delete, "/guilds/$GuildId/roles/$RoleId", NoStrategy) object GuildPruneCountGet - : Route(HttpMethod.Get, "/guilds/$GuildId/prune", GetPruneResponse.serializer()) + : Route( + HttpMethod.Get, + "/guilds/$GuildId/prune", + GetPruneResponse.serializer() + ) object GuildPrunePost - : Route(HttpMethod.Post, "/guilds/$GuildId/prune", PruneResponse.serializer()) + : + Route(HttpMethod.Post, "/guilds/$GuildId/prune", PruneResponse.serializer()) object GuildVoiceRegionsGet : Route>( @@ -316,27 +416,51 @@ sealed class Route( : Route(HttpMethod.Delete, "/guilds/$GuildId/integrations/$IntegrationId", NoStrategy) object GuildIntegrationSyncPost - : Route(HttpMethod.Post, "/guilds/$GuildId/integrations/$IntegrationId/sync", NoStrategy) + : Route( + HttpMethod.Post, + "/guilds/$GuildId/integrations/$IntegrationId/sync", + NoStrategy + ) @DeprecatedSinceKord("0.7.0") - @Deprecated("Guild embeds were renamed to widgets.", ReplaceWith("GuildWidgetGet"), DeprecationLevel.ERROR) + @Deprecated( + "Guild embeds were renamed to widgets.", + ReplaceWith("GuildWidgetGet"), + DeprecationLevel.ERROR + ) object GuildEmbedGet : Route(HttpMethod.Get, "/guilds/$GuildId/embed", NothingSerializer) @DeprecatedSinceKord("0.7.0") - @Deprecated("Guild embeds were renamed to widgets.", ReplaceWith("GuildWidgetPatch"), DeprecationLevel.ERROR) + @Deprecated( + "Guild embeds were renamed to widgets.", + ReplaceWith("GuildWidgetPatch"), + DeprecationLevel.ERROR + ) object GuildEmbedPatch : Route(HttpMethod.Patch, "/guilds/$GuildId/embed", NothingSerializer) object GuildWidgetGet - : Route(HttpMethod.Get, "/guilds/$GuildId/widget", DiscordGuildWidget.serializer()) + : Route( + HttpMethod.Get, + "/guilds/$GuildId/widget", + DiscordGuildWidget.serializer() + ) object GuildWidgetPatch - : Route(HttpMethod.Patch, "/guilds/$GuildId/widget", DiscordGuildWidget.serializer()) + : Route( + HttpMethod.Patch, + "/guilds/$GuildId/widget", + DiscordGuildWidget.serializer() + ) object GuildVanityInviteGet - : Route(HttpMethod.Get, "/guilds/$GuildId/vanity-url", DiscordPartialInvite.serializer()) + : Route( + HttpMethod.Get, + "/guilds/$GuildId/vanity-url", + DiscordPartialInvite.serializer() + ) object GuildWelcomeScreenGet : Route(HttpMethod.Get, "/guilds/${GuildId}/welcome-screen", DiscordWelcomeScreen.serializer()) @@ -367,7 +491,11 @@ sealed class Route( * This endpoint is only for Public guilds. */ object GuildPreviewGet - : Route(HttpMethod.Get, "/guilds/${GuildId}/preview", DiscordGuildPreview.serializer()) + : Route( + HttpMethod.Get, + "/guilds/${GuildId}/preview", + DiscordGuildPreview.serializer() + ) object ChannelWebhooksGet : Route>( @@ -387,16 +515,29 @@ sealed class Route( : Route(HttpMethod.Get, "/webhooks/$WebhookId", DiscordWebhook.serializer()) object WebhookPost - : Route(HttpMethod.Post, "/channels/$ChannelId/webhooks", DiscordWebhook.serializer()) + : Route( + HttpMethod.Post, + "/channels/$ChannelId/webhooks", + DiscordWebhook.serializer() + ) object WebhookByTokenGet - : Route(HttpMethod.Get, "/webhooks/$WebhookId/$WebhookToken", DiscordWebhook.serializer()) + : Route( + HttpMethod.Get, + "/webhooks/$WebhookId/$WebhookToken", + DiscordWebhook.serializer() + ) object WebhookPatch - : Route(HttpMethod.Patch, "/webhooks/$WebhookId", DiscordWebhook.serializer()) + : + Route(HttpMethod.Patch, "/webhooks/$WebhookId", DiscordWebhook.serializer()) object WebhookByTokenPatch - : Route(HttpMethod.Patch, "/webhooks/$WebhookId/$WebhookToken", DiscordWebhook.serializer()) + : Route( + HttpMethod.Patch, + "/webhooks/$WebhookId/$WebhookToken", + DiscordWebhook.serializer() + ) object WebhookDelete : Route(HttpMethod.Delete, "/webhooks/$WebhookId", NoStrategy) @@ -419,6 +560,12 @@ sealed class Route( object ExecuteGithubWebhookPost : Route(HttpMethod.Post, "/webhooks/$WebhookId/$WebhookToken", NoStrategy) + object EditWebhookMessage : Route( + HttpMethod.Patch, + "/webhooks/$WebhookId/$WebhookToken/messages/$MessageId", + DiscordMessage.serializer() + ) + object VoiceRegionsGet : Route>( HttpMethod.Get, @@ -428,32 +575,66 @@ sealed class Route( object CurrentApplicationInfo : - Route(HttpMethod.Get, "/oauth2/applications/@me", ApplicationInfoResponse.serializer()) + Route( + HttpMethod.Get, + "/oauth2/applications/@me", + ApplicationInfoResponse.serializer() + ) object TemplateGet - : Route(HttpMethod.Get, "guilds/templates/${TemplateCode}", DiscordTemplate.serializer()) + : Route( + HttpMethod.Get, + "guilds/templates/${TemplateCode}", + DiscordTemplate.serializer() + ) object GuildFromTemplatePost - : Route(HttpMethod.Post, "guilds/templates/${TemplateCode}", DiscordGuild.serializer()) + : Route( + HttpMethod.Post, + "guilds/templates/${TemplateCode}", + DiscordGuild.serializer() + ) object GuildTemplatesGet - : Route>(HttpMethod.Get, "/guilds/${GuildId}/templates", ListSerializer(DiscordTemplate.serializer())) + : Route>( + HttpMethod.Get, + "/guilds/${GuildId}/templates", + ListSerializer(DiscordTemplate.serializer()) + ) object GuildTemplatePost - : Route(HttpMethod.Post, "/guilds/${GuildId}/templates", DiscordTemplate.serializer()) + : Route( + HttpMethod.Post, + "/guilds/${GuildId}/templates", + DiscordTemplate.serializer() + ) object TemplateSyncPut - : Route(HttpMethod.Put, "/guilds/${GuildId}/templates/${TemplateCode}", DiscordTemplate.serializer()) + : Route( + HttpMethod.Put, + "/guilds/${GuildId}/templates/${TemplateCode}", + DiscordTemplate.serializer() + ) object TemplatePatch - : Route(HttpMethod.Patch, "/guilds/${GuildId}/templates/${TemplateCode}", DiscordTemplate.serializer()) + : Route( + HttpMethod.Patch, + "/guilds/${GuildId}/templates/${TemplateCode}", + DiscordTemplate.serializer() + ) object TemplateDelete - : Route(HttpMethod.Delete, "/guilds/${GuildId}/templates/${TemplateCode}", DiscordTemplate.serializer()) + : Route( + HttpMethod.Delete, + "/guilds/${GuildId}/templates/${TemplateCode}", + DiscordTemplate.serializer() + ) object GlobalApplicationCommandsGet : Route>( - HttpMethod.Get, "/applications/${ApplicationId}/commands", ListSerializer(DiscordApplicationCommand.serializer()) + HttpMethod.Get, + "/applications/${ApplicationId}/commands", + ListSerializer(DiscordApplicationCommand.serializer()) ) object GlobalApplicationCommandCreate : Route( @@ -528,7 +709,11 @@ sealed class Route( ) object OriginalInteractionResponseDelete - : Route(HttpMethod.Delete, "/webhooks/${ApplicationId}/${InteractionToken}/messages/@original", NoStrategy) + : Route( + HttpMethod.Delete, + "/webhooks/${ApplicationId}/${InteractionToken}/messages/@original", + NoStrategy + ) object FollowupMessageCreate : Route( HttpMethod.Post, @@ -543,7 +728,11 @@ sealed class Route( ) object FollowupMessageDelete : - Route(HttpMethod.Delete, "/webhooks/${ApplicationId}/${InteractionToken}/messages/${MessageId}", NoStrategy) + Route( + HttpMethod.Delete, + "/webhooks/${ApplicationId}/${InteractionToken}/messages/${MessageId}", + NoStrategy + ) companion object { val baseUrl = "https://discord.com/api/$restVersion" diff --git a/rest/src/main/kotlin/service/WebhookService.kt b/rest/src/main/kotlin/service/WebhookService.kt index debc239d743..5c7376bacb1 100644 --- a/rest/src/main/kotlin/service/WebhookService.kt +++ b/rest/src/main/kotlin/service/WebhookService.kt @@ -4,10 +4,12 @@ import dev.kord.common.annotation.KordExperimental import dev.kord.common.entity.DiscordMessage import dev.kord.common.entity.DiscordWebhook import dev.kord.common.entity.Snowflake +import dev.kord.rest.builder.webhook.EditWebhookMessageBuilder import dev.kord.rest.builder.webhook.ExecuteWebhookBuilder import dev.kord.rest.builder.webhook.WebhookCreateBuilder import dev.kord.rest.builder.webhook.WebhookModifyBuilder 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.RequestHandler @@ -20,7 +22,11 @@ import kotlin.contracts.contract class WebhookService(requestHandler: RequestHandler) : RestService(requestHandler) { @OptIn(ExperimentalContracts::class) - suspend inline fun createWebhook(channelId: Snowflake, name: String, builder: WebhookCreateBuilder.() -> Unit): DiscordWebhook { + suspend inline fun createWebhook( + channelId: Snowflake, + name: String, + builder: WebhookCreateBuilder.() -> Unit + ): DiscordWebhook { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } @@ -45,13 +51,17 @@ class WebhookService(requestHandler: RequestHandler) : RestService(requestHandle keys[Route.WebhookId] = webhookId } - suspend fun getWebhookWithToken(webhookId: Snowflake, token: String) = call(Route.WebhookByTokenGet) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token - } + suspend fun getWebhookWithToken(webhookId: Snowflake, token: String) = + call(Route.WebhookByTokenGet) { + keys[Route.WebhookId] = webhookId + keys[Route.WebhookToken] = token + } @OptIn(ExperimentalContracts::class) - suspend inline fun modifyWebhook(webhookId: Snowflake, builder: WebhookModifyBuilder.() -> Unit): DiscordWebhook { + suspend inline fun modifyWebhook( + webhookId: Snowflake, + builder: WebhookModifyBuilder.() -> Unit + ): DiscordWebhook { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } @@ -65,7 +75,11 @@ class WebhookService(requestHandler: RequestHandler) : RestService(requestHandle } @OptIn(ExperimentalContracts::class) - suspend inline fun modifyWebhookWithToken(webhookId: Snowflake, token: String, builder: WebhookModifyBuilder.() -> Unit): DiscordWebhook { + suspend inline fun modifyWebhookWithToken( + webhookId: Snowflake, + token: String, + builder: WebhookModifyBuilder.() -> Unit + ): DiscordWebhook { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } @@ -79,19 +93,29 @@ class WebhookService(requestHandler: RequestHandler) : RestService(requestHandle } } - suspend fun deleteWebhook(webhookId: Snowflake, reason: String? = null) = call(Route.WebhookDelete) { - keys[Route.WebhookId] = webhookId - reason?.let { header("X-Audit-Log-Reason", reason) } - } + suspend fun deleteWebhook(webhookId: Snowflake, reason: String? = null) = + call(Route.WebhookDelete) { + keys[Route.WebhookId] = webhookId + reason?.let { header("X-Audit-Log-Reason", reason) } + } - suspend fun deleteWebhookWithToken(webhookId: Snowflake, token: String, reason: String? = null) = call(Route.WebhookByTokenDelete) { + suspend fun deleteWebhookWithToken( + webhookId: Snowflake, + token: String, + reason: String? = null + ) = call(Route.WebhookByTokenDelete) { keys[Route.WebhookId] = webhookId keys[Route.WebhookToken] = token reason?.let { header("X-Audit-Log-Reason", reason) } } @OptIn(ExperimentalContracts::class) - suspend inline fun executeWebhook(webhookId: Snowflake, token: String, wait: Boolean, builder: ExecuteWebhookBuilder.() -> Unit): DiscordMessage? { + suspend inline fun executeWebhook( + webhookId: Snowflake, + token: String, + wait: Boolean, + builder: ExecuteWebhookBuilder.() -> Unit + ): DiscordMessage? { contract { callsInPlace(builder, InvocationKind.EXACTLY_ONCE) } @@ -107,20 +131,51 @@ class WebhookService(requestHandler: RequestHandler) : RestService(requestHandle } @KordExperimental - suspend fun executeSlackWebhook(webhookId: Snowflake, token: String, body: JsonObject, wait: Boolean = false) = - call(Route.ExecuteSlackWebhookPost) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token - parameter("wait", "$wait") - body(JsonObject.serializer(), body) - } + suspend fun executeSlackWebhook( + webhookId: Snowflake, + token: String, + body: JsonObject, + wait: Boolean = false + ) = + call(Route.ExecuteSlackWebhookPost) { + keys[Route.WebhookId] = webhookId + keys[Route.WebhookToken] = token + parameter("wait", "$wait") + body(JsonObject.serializer(), body) + } @KordExperimental - suspend fun executeGithubWebhook(webhookId: Snowflake, token: String, body: JsonObject, wait: Boolean = false) = - call(Route.ExecuteGithubWebhookPost) { - keys[Route.WebhookId] = webhookId - keys[Route.WebhookToken] = token - parameter("wait", "$wait") - body(JsonObject.serializer(), body) - } + suspend fun executeGithubWebhook( + webhookId: Snowflake, + token: String, + body: JsonObject, + wait: Boolean = false + ) = + call(Route.ExecuteGithubWebhookPost) { + keys[Route.WebhookId] = webhookId + keys[Route.WebhookToken] = token + parameter("wait", "$wait") + body(JsonObject.serializer(), body) + } + + @OptIn(ExperimentalContracts::class) + suspend inline fun editWebhookMessage( + webhookId: Snowflake, + token: String, + messageId: Snowflake, + builder: EditWebhookMessageBuilder.() -> Unit + ): DiscordMessage { + contract { + callsInPlace(builder, InvocationKind.EXACTLY_ONCE) + } + + return call(Route.EditWebhookMessage) { + + keys[Route.WebhookId] = webhookId + keys[Route.WebhookToken] = token + keys[Route.MessageId] = messageId + val body = EditWebhookMessageBuilder().apply(builder).toRequest() + body(WebhookEditMessageRequest.serializer(), body) + } + } } \ No newline at end of file