Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add common MessageBuilder supertype #891

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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