Skip to content

Commit

Permalink
Add common MessageBuilder supertype
Browse files Browse the repository at this point in the history
MessageBuilder is the new common supertype of MessageCreateBuilder and
MessageModifyBuilder.

The support for attachments was improved by adding AttachmentBuilder,
which can be configured when adding files to messages.

The way multipart requests are sent is now also more closely aligned
with the documentation [1].

[1] https://discord.com/developers/docs/reference#uploading-files
  • Loading branch information
lukellmann committed Nov 20, 2023
1 parent f8d4333 commit 6c69060
Show file tree
Hide file tree
Showing 27 changed files with 856 additions and 852 deletions.
1 change: 1 addition & 0 deletions common/api/common.api
Original file line number Diff line number Diff line change
Expand Up @@ -9182,6 +9182,7 @@ public final class dev/kord/common/entity/optional/OptionalKt {
public static final fun mapList (Ldev/kord/common/entity/optional/Optional;Lkotlin/jvm/functions/Function1;)Ldev/kord/common/entity/optional/Optional;
public static final fun mapNotNull (Ldev/kord/common/entity/optional/Optional;Lkotlin/jvm/functions/Function1;)Ldev/kord/common/entity/optional/Optional;
public static final fun mapNullable (Ldev/kord/common/entity/optional/Optional;Lkotlin/jvm/functions/Function1;)Ldev/kord/common/entity/optional/Optional;
public static final fun mapNullableList (Ldev/kord/common/entity/optional/Optional;Lkotlin/jvm/functions/Function1;)Ldev/kord/common/entity/optional/Optional;
public static final fun mapNullableOptional (Ldev/kord/common/entity/optional/Optional;Lkotlin/jvm/functions/Function1;)Ldev/kord/common/entity/optional/Optional;
public static final fun mapNullableSnowflake (Ldev/kord/common/entity/optional/Optional;Lkotlin/jvm/functions/Function1;)Ldev/kord/common/entity/optional/OptionalSnowflake;
public static final fun mapSnowflake (Ldev/kord/common/entity/optional/Optional;Lkotlin/jvm/functions/Function1;)Ldev/kord/common/entity/optional/OptionalSnowflake;
Expand Down
7 changes: 7 additions & 0 deletions common/src/commonMain/kotlin/entity/optional/Optional.kt
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,13 @@ public inline fun <E, T> Optional<List<E>>.mapList(mapper: (E) -> T): Optional<L
is Value -> Value(value.map(mapper))
}

@JvmName("mapNullableList")
public inline fun <E, T> Optional<List<E>?>.mapList(mapper: (E) -> T): Optional<List<T>?> = when (this) {
is Missing -> Missing()
is Null -> Null()
is Value -> Value(value!!.map(mapper))
}

public fun <T> Optional<MutableList<T>>.mapCopy(): Optional<List<T>> = map { mutable -> mutable.toList() }

@JvmName("mapCopyOfMap")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import dev.kord.core.supplier.EntitySupplyStrategy
import dev.kord.rest.builder.message.EmbedBuilder
import dev.kord.rest.builder.message.create.MessageCreateBuilder
import dev.kord.rest.builder.message.create.UserMessageCreateBuilder
import dev.kord.rest.builder.message.create.embed
import dev.kord.rest.builder.message.embed
import dev.kord.rest.request.RestRequestException
import dev.kord.rest.service.RestClient
import kotlinx.coroutines.coroutineScope
Expand Down
471 changes: 213 additions & 258 deletions rest/api/rest.api

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions rest/src/commonMain/kotlin/builder/message/AttachmentBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.kord.rest.builder.message

import dev.kord.common.annotation.KordDsl
import dev.kord.common.entity.Snowflake
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.json.request.AttachmentRequest

@KordDsl
public class AttachmentBuilder(private val id: Snowflake) : RequestBuilder<AttachmentRequest> {

private var _filename: Optional<String> = Optional.Missing()

/** The name of the attached file. */
public var filename: String? by ::_filename.delegate()

private var _description: Optional<String> = Optional.Missing()

/** The description for the file (max 1024 characters). */
public var description: String? by ::_description.delegate()

override fun toRequest(): AttachmentRequest = AttachmentRequest(
id = id,
filename = _filename,
description = _description,
)
}
147 changes: 147 additions & 0 deletions rest/src/commonMain/kotlin/builder/message/MessageBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package dev.kord.rest.builder.message

import dev.kord.common.annotation.KordDsl
import dev.kord.common.entity.MessageFlag
import dev.kord.common.entity.MessageFlags
import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.optional.Optional
import dev.kord.rest.NamedFile
import dev.kord.rest.builder.component.ActionRowBuilder
import dev.kord.rest.builder.component.MessageComponentBuilder
import dev.kord.rest.request.MultipartRequest
import io.ktor.client.request.forms.*
import kotlin.contracts.InvocationKind.EXACTLY_ONCE
import kotlin.contracts.contract

@KordDsl
public interface MessageBuilder {

/** The message contents (up to 2000 characters). */
public var content: String?

/** Up to 10 embeds (up to 6000 characters). */
public var embeds: MutableList<EmbedBuilder>?

/**
* The mentions in the message that are allowed to trigger a ping.
*
* Setting this to `null` will default to triggering pings for all mentions.
*/
public var allowedMentions: AllowedMentionsBuilder?

/** The components to include with the message.*/
public var components: MutableList<MessageComponentBuilder>?

/** The files to include as attachments. */
public val files: MutableList<NamedFile>

/**
* The attachment objects with [filename][AttachmentBuilder.filename] and
* [description][AttachmentBuilder.description].
*/
public var attachments: MutableList<AttachmentBuilder>?

/**
* Optional custom [MessageFlags].
*
* @see suppressEmbeds
*/
public var flags: MessageFlags?

/** Do not include any embeds when serializing this message. */
public var suppressEmbeds: Boolean?

/** Adds a [file][NamedFile] with [name] and [contentProvider] to [files]. */
public fun addFile(name: String, contentProvider: ChannelProvider): NamedFile {
val file = NamedFile(name, contentProvider)
files.add(file)
return file
}
}

/**
* Adds an [embed][EmbedBuilder] configured by [builder] to the [embeds][MessageBuilder.embeds] of the message.
*
* A message can have up to 10 embeds.
*/
public inline fun MessageBuilder.embed(builder: EmbedBuilder.() -> Unit) {
contract { callsInPlace(builder, EXACTLY_ONCE) }
val embed = EmbedBuilder().apply(builder)
embeds?.add(embed) ?: run { embeds = mutableListOf(embed) }
}

/**
* Configures the mentions in the message that are allowed to trigger a ping.
*
* Not calling this function will result in the default behavior (ping for all mentions), calling this function but not
* configuring it before the request is build will result in all mentions being ignored.
*/
public inline fun MessageBuilder.allowedMentions(builder: AllowedMentionsBuilder.() -> Unit = {}) {
contract { callsInPlace(builder, EXACTLY_ONCE) }
val mentions = allowedMentions ?: (AllowedMentionsBuilder().also { allowedMentions = it })
mentions.builder()
}

/**
* Adds an [action row][ActionRowBuilder] configured by the [builder] to the [components][MessageBuilder.components] of
* the message.
*
* A message can have up to five action rows.
*/
public inline fun MessageBuilder.actionRow(builder: ActionRowBuilder.() -> Unit) {
contract { callsInPlace(builder, EXACTLY_ONCE) }
val actionRow = ActionRowBuilder().apply(builder)
components?.add(actionRow) ?: run { components = mutableListOf(actionRow) }
}

/**
* Adds a [file][NamedFile] with [name] and [contentProvider] to [files][MessageBuilder.files].
*
* The corresponding attachment object can be configured with [builder].
*/
public inline fun MessageBuilder.addFile(
name: String,
contentProvider: ChannelProvider,
builder: AttachmentBuilder.() -> Unit,
): NamedFile {
contract { callsInPlace(builder, EXACTLY_ONCE) }
// see https://discord.com/developers/docs/reference#uploading-files:
// we use the index of a file in the `files` list as `n` in `files[n]`, as implemented in `MultipartRequest.data`
/** (clickable link: [MultipartRequest.data]) */
val file = NamedFile(name, contentProvider)
files.add(file)
val index = files.lastIndex
val attachment = AttachmentBuilder(id = Snowflake(index.toLong())).apply(builder)
attachments?.add(attachment) ?: run { attachments = mutableListOf(attachment) }
return file
}

/** Sets the [flags][MessageBuilder.flags] for the message. */
public inline fun MessageBuilder.messageFlags(builder: MessageFlags.Builder.() -> Unit) {
contract { callsInPlace(builder, EXACTLY_ONCE) }
flags = MessageFlags(builder)
}


internal fun buildMessageFlags(
base: MessageFlags?,
suppressEmbeds: Boolean?,
suppressNotifications: Boolean? = null,
ephemeral: Boolean? = null,
): Optional<MessageFlags> =
if (base == null && suppressEmbeds == null && suppressNotifications == null && ephemeral == null) {
Optional.Missing()
} else {
val flags = MessageFlags {
if (base != null) +base
fun apply(add: Boolean?, flag: MessageFlag) = when (add) {
true -> +flag
false -> -flag
null -> {}
}
apply(suppressEmbeds, MessageFlag.SuppressEmbeds)
apply(suppressNotifications, MessageFlag.SuppressNotifications)
apply(ephemeral, MessageFlag.Ephemeral)
}
Optional.Value(flags)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package dev.kord.rest.builder.message.create

import dev.kord.common.annotation.KordDsl
import dev.kord.common.entity.MessageFlags
import dev.kord.common.entity.optional.*
import dev.kord.rest.NamedFile
import dev.kord.common.entity.optional.map
import dev.kord.common.entity.optional.mapList
import dev.kord.rest.builder.RequestBuilder
import dev.kord.rest.builder.component.MessageComponentBuilder
import dev.kord.rest.builder.message.AllowedMentionsBuilder
import dev.kord.rest.builder.message.EmbedBuilder
import dev.kord.rest.builder.message.buildMessageFlags
import dev.kord.rest.json.request.FollowupMessageCreateRequest
import dev.kord.rest.json.request.MultipartFollowupMessageCreateRequest

Expand All @@ -16,38 +13,19 @@ import dev.kord.rest.json.request.MultipartFollowupMessageCreateRequest
*/
@KordDsl
public class FollowupMessageCreateBuilder(public val ephemeral: Boolean) :
MessageCreateBuilder,
AbstractMessageCreateBuilder(),
RequestBuilder<MultipartFollowupMessageCreateRequest> {

override var content: String? = null

override var tts: Boolean? = null

override val embeds: MutableList<EmbedBuilder> = mutableListOf()

override var allowedMentions: AllowedMentionsBuilder? = null


override val components: MutableList<MessageComponentBuilder> = mutableListOf()

override val files: MutableList<NamedFile> = mutableListOf()

override var flags: MessageFlags? = null
override var suppressEmbeds: Boolean? = null
override var suppressNotifications: Boolean? = null

override fun toRequest(): MultipartFollowupMessageCreateRequest {
return MultipartFollowupMessageCreateRequest(
FollowupMessageCreateRequest(
content = Optional(content).coerceToMissing(),
tts = Optional(tts).coerceToMissing().toPrimitive(),
embeds = Optional(embeds).mapList { it.toRequest() },
allowedMentions = Optional(allowedMentions).coerceToMissing().map { it.build() },
components = Optional(components).coerceToMissing().mapList { it.build() },
flags = buildMessageFlags(flags, suppressEmbeds, suppressNotifications, ephemeral)
),
files
)
}

// see https://discord.com/developers/docs/interactions/receiving-and-responding#create-followup-message
override fun toRequest(): MultipartFollowupMessageCreateRequest = MultipartFollowupMessageCreateRequest(
request = FollowupMessageCreateRequest(
content = _content,
tts = _tts,
embeds = _embeds.mapList { it.toRequest() },
allowedMentions = _allowedMentions.map { it.build() },
components = _components.mapList { it.build() },
attachments = _attachments.mapList { it.toRequest() },
flags = buildMessageFlags(flags, suppressEmbeds, suppressNotifications, ephemeral),
),
files = files.toList(),
)
}
Original file line number Diff line number Diff line change
@@ -1,55 +1,44 @@
package dev.kord.rest.builder.message.create

import dev.kord.common.annotation.KordDsl
import dev.kord.common.entity.MessageFlags
import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.coerceToMissing
import dev.kord.common.entity.optional.delegate.delegate
import dev.kord.common.entity.optional.map
import dev.kord.common.entity.optional.mapCopy
import dev.kord.common.entity.optional.mapList
import dev.kord.rest.NamedFile
import dev.kord.rest.builder.RequestBuilder
import dev.kord.rest.builder.component.MessageComponentBuilder
import dev.kord.rest.builder.message.AllowedMentionsBuilder
import dev.kord.rest.builder.message.EmbedBuilder
import dev.kord.rest.builder.message.buildMessageFlags
import dev.kord.rest.json.request.ForumThreadMessageRequest
import dev.kord.rest.json.request.MultipartForumThreadMessageCreateRequest

@KordDsl
public class ForumMessageCreateBuilder : MessageCreateBuilder,
public class ForumMessageCreateBuilder :
AbstractMessageCreateBuilder(),
RequestBuilder<MultipartForumThreadMessageCreateRequest> {

override var content: String? = null

override var tts: Boolean? = null

override val embeds: MutableList<EmbedBuilder> = mutableListOf()

override var allowedMentions: AllowedMentionsBuilder? = null

override val components: MutableList<MessageComponentBuilder> = mutableListOf()

override val files: MutableList<NamedFile> = mutableListOf()
// see https://discord.com/developers/docs/resources/channel#start-thread-in-forum-or-media-channel

private var _stickerIds: Optional<MutableList<Snowflake>> = Optional.Missing()
public val stickerIds: MutableList<Snowflake>? by ::_stickerIds.delegate()

override var flags: MessageFlags? = null
override var suppressEmbeds: Boolean? = null
override var suppressNotifications: Boolean? = null
/** The IDs of up to three stickers to send in the message. */
public var stickerIds: MutableList<Snowflake>? by ::_stickerIds.delegate()

override fun toRequest(): MultipartForumThreadMessageCreateRequest = MultipartForumThreadMessageCreateRequest(
request = ForumThreadMessageRequest(
content = _content,
tts = _tts,
embeds = _embeds.mapList { it.toRequest() },
allowedMentions = _allowedMentions.map { it.build() },
components = _components.mapList { it.build() },
stickerIds = _stickerIds.mapCopy(),
attachments = _attachments.mapList { it.toRequest() },
flags = buildMessageFlags(flags, suppressEmbeds, suppressNotifications),
),
files = files.toList(),
)
}

override fun toRequest(): MultipartForumThreadMessageCreateRequest {
return MultipartForumThreadMessageCreateRequest(
ForumThreadMessageRequest(
content = Optional(content).coerceToMissing(),
embeds = Optional(embeds).mapList { it.toRequest() },
allowedMentions = Optional(allowedMentions).coerceToMissing().map { it.build() },
components = Optional(components).coerceToMissing().mapList { it.build() },
stickerIds = _stickerIds,
flags = buildMessageFlags(flags, suppressEmbeds, suppressNotifications),
),
files
)
}
/** Add a [stickerId] to [stickerIds][ForumMessageCreateBuilder.stickerIds]. */
public fun ForumMessageCreateBuilder.stickerId(stickerId: Snowflake) {
stickerIds?.add(stickerId) ?: run { stickerIds = mutableListOf(stickerId) }
}
Loading

0 comments on commit 6c69060

Please sign in to comment.