mirror of
https://github.com/InsanusMokrassar/TelegramBotAPI.git
synced 2026-08-19 15:46:31 +00:00
Add Bot API 10.2 Rich Messages support
Input-side rich messages: InputRichMessage.blocks/media fields, 21 InputRichBlock* block types + InputRichBlockSerializer, InputRichBlockListItem, InputRichMessageMedia, TelegramMediaVoiceNote + RichMessageMemberTelegramMedia marker on Animation/Audio/Photo/Video, DSL builders, and multipart upload for SendRichMessage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
package dev.inmo.tgbotapi.requests.send
|
||||
|
||||
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
|
||||
import dev.inmo.tgbotapi.requests.abstracts.MultipartRequest
|
||||
import dev.inmo.tgbotapi.requests.abstracts.SimpleRequest
|
||||
import dev.inmo.tgbotapi.requests.send.abstracts.ReplyingMarkupSendMessageRequest
|
||||
import dev.inmo.tgbotapi.requests.send.abstracts.SendContentMessageRequest
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
@@ -10,6 +13,7 @@ import dev.inmo.tgbotapi.types.message.abstracts.ChatContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.TelegramBotAPIMessageDeserializationStrategyClass
|
||||
import dev.inmo.tgbotapi.types.message.content.RichMessageContent
|
||||
import dev.inmo.tgbotapi.types.rich.InputRichMessage
|
||||
import dev.inmo.tgbotapi.types.rich.multipartFiles
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
@@ -56,10 +60,14 @@ data class SendRichMessage(
|
||||
@SerialName(replyMarkupField)
|
||||
override val replyMarkup: KeyboardMarkup? = null
|
||||
) : SendContentMessageRequest<ChatContentMessage<RichMessageContent>>,
|
||||
ReplyingMarkupSendMessageRequest<ChatContentMessage<RichMessageContent>> {
|
||||
ReplyingMarkupSendMessageRequest<ChatContentMessage<RichMessageContent>>,
|
||||
MultipartRequest.Common<ChatContentMessage<RichMessageContent>> {
|
||||
override fun method(): String = "sendRichMessage"
|
||||
override val resultDeserializer: DeserializationStrategy<ChatContentMessage<RichMessageContent>>
|
||||
get() = RichMessageContentMessageResultDeserializer
|
||||
override val requestSerializer: SerializationStrategy<*>
|
||||
get() = serializer()
|
||||
override val data: SimpleRequest<ChatContentMessage<RichMessageContent>>
|
||||
get() = this
|
||||
override val mediaMap: Map<String, MultipartFile> by lazy { richMessage.multipartFiles }
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import dev.inmo.tgbotapi.types.draftIdField
|
||||
import dev.inmo.tgbotapi.types.messageThreadIdField
|
||||
import dev.inmo.tgbotapi.types.richMessageField
|
||||
import dev.inmo.tgbotapi.types.rich.InputRichMessage
|
||||
import dev.inmo.tgbotapi.types.rich.multipartFiles
|
||||
import dev.inmo.tgbotapi.utils.serializers.UnitFromBooleanSerializer
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -39,6 +40,9 @@ data class SendRichMessageDraft(
|
||||
require(draftId != 0L) {
|
||||
"draftId of SendRichMessageDraft must be non-zero"
|
||||
}
|
||||
require(richMessage.multipartFiles.isEmpty()) {
|
||||
"sendRichMessageDraft does not support direct upload of new files"
|
||||
}
|
||||
}
|
||||
|
||||
override val requestSerializer: SerializationStrategy<*>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.tgbotapi.types.media
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
@Serializable(TelegramMediaSerializer::class)
|
||||
sealed interface RichMessageMemberTelegramMedia : TelegramMedia
|
||||
@@ -67,7 +67,7 @@ data class TelegramMediaAnimation internal constructor(
|
||||
override val height: Int? = null,
|
||||
override val duration: Long? = null,
|
||||
override val thumb: InputFile? = null
|
||||
) : TelegramFreeMedia, SizedTelegramMedia, DuratedTelegramMedia, ThumbedTelegramMedia, TextedOutput, SpoilerableTelegramMedia, WithCustomizableCaptionTelegramMedia, InputPollMedia, InputPollOptionMedia {
|
||||
) : TelegramFreeMedia, RichMessageMemberTelegramMedia, SizedTelegramMedia, DuratedTelegramMedia, ThumbedTelegramMedia, TextedOutput, SpoilerableTelegramMedia, WithCustomizableCaptionTelegramMedia, InputPollMedia, InputPollOptionMedia {
|
||||
@EncodeDefault
|
||||
override val type: String = TYPE
|
||||
override val textSources: TextSourcesList? by lazy {
|
||||
|
||||
@@ -51,7 +51,7 @@ data class TelegramMediaAudio internal constructor(
|
||||
override val performer: String? = null,
|
||||
override val title: String? = null,
|
||||
override val thumb: InputFile? = null
|
||||
) : TelegramFreeMedia, AudioMediaGroupMemberTelegramMedia, DuratedTelegramMedia, ThumbedTelegramMedia, TitledTelegramMedia,
|
||||
) : TelegramFreeMedia, RichMessageMemberTelegramMedia, AudioMediaGroupMemberTelegramMedia, DuratedTelegramMedia, ThumbedTelegramMedia, TitledTelegramMedia,
|
||||
Performerable, InputPollMedia {
|
||||
@EncodeDefault
|
||||
override val type: String = TYPE
|
||||
|
||||
@@ -44,7 +44,7 @@ data class TelegramMediaPhoto internal constructor(
|
||||
override val spoilered: Boolean = false,
|
||||
@SerialName(showCaptionAboveMediaField)
|
||||
override val showCaptionAboveMedia: Boolean = false,
|
||||
) : TelegramFreeMedia, VisualMediaGroupMemberTelegramMedia, InputPollMedia, InputPollOptionMedia {
|
||||
) : TelegramFreeMedia, RichMessageMemberTelegramMedia, VisualMediaGroupMemberTelegramMedia, InputPollMedia, InputPollOptionMedia {
|
||||
@EncodeDefault
|
||||
override val type: String = TYPE
|
||||
override val textSources: TextSourcesList? by lazy {
|
||||
|
||||
@@ -25,6 +25,7 @@ object TelegramMediaSerializer : KSerializer<TelegramMedia> {
|
||||
is TelegramPaidMediaPhoto -> TelegramPaidMediaPhoto.serializer().serialize(encoder, value)
|
||||
is TelegramPaidMediaLivePhoto -> TelegramPaidMediaLivePhoto.serializer().serialize(encoder, value)
|
||||
is TelegramMediaSticker -> TelegramMediaSticker.serializer().serialize(encoder, value)
|
||||
is TelegramMediaVoiceNote -> TelegramMediaVoiceNote.serializer().serialize(encoder, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ data class TelegramMediaVideo internal constructor (
|
||||
@SerialName(supportsStreamingField)
|
||||
override val supportsStreaming: Boolean = false,
|
||||
) : TelegramFreeMedia,
|
||||
RichMessageMemberTelegramMedia,
|
||||
SizedTelegramMedia,
|
||||
DuratedTelegramMedia,
|
||||
ThumbedTelegramMedia,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package dev.inmo.tgbotapi.types.media
|
||||
|
||||
import dev.inmo.tgbotapi.abstracts.TextedOutput
|
||||
import dev.inmo.tgbotapi.requests.abstracts.InputFile
|
||||
import dev.inmo.tgbotapi.requests.abstracts.fileIdToSend
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.message.textsources.TextSourcesList
|
||||
import dev.inmo.tgbotapi.types.message.ParseMode
|
||||
import dev.inmo.tgbotapi.types.message.parseModeField
|
||||
import dev.inmo.tgbotapi.types.files.VoiceFile
|
||||
import dev.inmo.tgbotapi.types.message.*
|
||||
import dev.inmo.tgbotapi.types.message.RawMessageEntity
|
||||
import dev.inmo.tgbotapi.types.message.toRawMessageEntities
|
||||
import dev.inmo.tgbotapi.utils.extensions.makeString
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
fun TelegramMediaVoiceNote(
|
||||
file: InputFile,
|
||||
entities: TextSourcesList,
|
||||
duration: Long? = null
|
||||
) = TelegramMediaVoiceNote(
|
||||
file, entities.makeString(), null, entities.toRawMessageEntities(), duration
|
||||
)
|
||||
|
||||
fun TelegramMediaVoiceNote(
|
||||
file: InputFile,
|
||||
text: String? = null,
|
||||
parseMode: ParseMode? = null,
|
||||
duration: Long? = null
|
||||
) = TelegramMediaVoiceNote(
|
||||
file, text, parseMode, null, duration
|
||||
)
|
||||
|
||||
@ConsistentCopyVisibility
|
||||
@Serializable
|
||||
data class TelegramMediaVoiceNote internal constructor(
|
||||
override val file: InputFile,
|
||||
@SerialName(captionField)
|
||||
override val text: String? = null,
|
||||
@SerialName(parseModeField)
|
||||
override val parseMode: ParseMode? = null,
|
||||
@SerialName(captionEntitiesField)
|
||||
private val rawEntities: List<RawMessageEntity>? = null,
|
||||
override val duration: Long? = null
|
||||
) : RichMessageMemberTelegramMedia, DuratedTelegramMedia, TextedOutput {
|
||||
@EncodeDefault
|
||||
override val type: String = TYPE
|
||||
override val textSources: TextSourcesList? by lazy {
|
||||
rawEntities ?.asTextSources(text ?: return@lazy null)
|
||||
}
|
||||
|
||||
@SerialName(mediaField)
|
||||
override val media: String
|
||||
init { media = file.fileIdToSend } // crutch until js compiling will be fixed
|
||||
|
||||
companion object {
|
||||
const val TYPE = "voice_note"
|
||||
}
|
||||
}
|
||||
|
||||
fun VoiceFile.toTelegramMediaVoiceNote(
|
||||
text: String? = null,
|
||||
parseMode: ParseMode? = null
|
||||
): TelegramMediaVoiceNote = TelegramMediaVoiceNote(
|
||||
fileId,
|
||||
text,
|
||||
parseMode,
|
||||
duration
|
||||
)
|
||||
|
||||
fun VoiceFile.toTelegramMediaVoiceNote(
|
||||
textSources: TextSourcesList = emptyList()
|
||||
): TelegramMediaVoiceNote = TelegramMediaVoiceNote(
|
||||
fileId,
|
||||
textSources,
|
||||
duration
|
||||
)
|
||||
@@ -0,0 +1,72 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMedia
|
||||
import dev.inmo.tgbotapi.types.typeField
|
||||
import dev.inmo.tgbotapi.utils.internal.ClassCastsIncluded
|
||||
import kotlinx.serialization.DeserializationStrategy
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
/**
|
||||
* Represents a block in a rich formatted message to be sent.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblock">InputRichBlock</a>
|
||||
*/
|
||||
@Serializable(InputRichBlockSerializer::class)
|
||||
@ClassCastsIncluded
|
||||
sealed interface InputRichBlock {
|
||||
val type: String
|
||||
}
|
||||
|
||||
@Serializable(InputRichBlockSerializer::class)
|
||||
sealed interface InputRichBlockMedia : InputRichBlock {
|
||||
val media: TelegramMedia
|
||||
val caption: RichBlockCaption?
|
||||
}
|
||||
|
||||
/**
|
||||
* The nested [InputRichBlock]s directly contained by this block, or an empty list for leaf blocks. Container blocks
|
||||
* ([InputRichBlockList] via its [InputRichBlockListItem.blocks], [InputRichBlockBlockQuotation], [InputRichBlockCollage],
|
||||
* [InputRichBlockSlideshow] and [InputRichBlockDetails]) expose their children here.
|
||||
*/
|
||||
val InputRichBlock.subBlocks: List<InputRichBlock>
|
||||
get() = when (this) {
|
||||
is InputRichBlockList -> items.flatMap { it.blocks }
|
||||
is InputRichBlockBlockQuotation -> blocks
|
||||
is InputRichBlockCollage -> blocks
|
||||
is InputRichBlockSlideshow -> blocks
|
||||
is InputRichBlockDetails -> blocks
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
object InputRichBlockSerializer : JsonContentPolymorphicSerializer<InputRichBlock>(InputRichBlock::class) {
|
||||
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<InputRichBlock> {
|
||||
return when (val type = element.jsonObject[typeField]?.jsonPrimitive?.content) {
|
||||
InputRichBlockParagraph.TYPE -> InputRichBlockParagraph.serializer()
|
||||
InputRichBlockSectionHeading.TYPE -> InputRichBlockSectionHeading.serializer()
|
||||
InputRichBlockPreformatted.TYPE -> InputRichBlockPreformatted.serializer()
|
||||
InputRichBlockFooter.TYPE -> InputRichBlockFooter.serializer()
|
||||
InputRichBlockDivider.TYPE -> InputRichBlockDivider.serializer()
|
||||
InputRichBlockMathematicalExpression.TYPE -> InputRichBlockMathematicalExpression.serializer()
|
||||
InputRichBlockAnchor.TYPE -> InputRichBlockAnchor.serializer()
|
||||
InputRichBlockList.TYPE -> InputRichBlockList.serializer()
|
||||
InputRichBlockBlockQuotation.TYPE -> InputRichBlockBlockQuotation.serializer()
|
||||
InputRichBlockPullQuotation.TYPE -> InputRichBlockPullQuotation.serializer()
|
||||
InputRichBlockCollage.TYPE -> InputRichBlockCollage.serializer()
|
||||
InputRichBlockSlideshow.TYPE -> InputRichBlockSlideshow.serializer()
|
||||
InputRichBlockTable.TYPE -> InputRichBlockTable.serializer()
|
||||
InputRichBlockDetails.TYPE -> InputRichBlockDetails.serializer()
|
||||
InputRichBlockMap.TYPE -> InputRichBlockMap.serializer()
|
||||
InputRichBlockAnimation.TYPE -> InputRichBlockAnimation.serializer()
|
||||
InputRichBlockAudio.TYPE -> InputRichBlockAudio.serializer()
|
||||
InputRichBlockPhoto.TYPE -> InputRichBlockPhoto.serializer()
|
||||
InputRichBlockVideo.TYPE -> InputRichBlockVideo.serializer()
|
||||
InputRichBlockVoiceNote.TYPE -> InputRichBlockVoiceNote.serializer()
|
||||
InputRichBlockThinking.TYPE -> InputRichBlockThinking.serializer()
|
||||
else -> error("Unknown InputRichBlock type: $type")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import dev.inmo.tgbotapi.types.blocksField
|
||||
import dev.inmo.tgbotapi.types.hasCheckboxField
|
||||
import dev.inmo.tgbotapi.types.isCheckedField
|
||||
import dev.inmo.tgbotapi.types.typeField
|
||||
import dev.inmo.tgbotapi.types.valueField
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* An item of an [InputRichBlockList].
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblocklistitem">InputRichBlockListItem</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockListItem(
|
||||
@SerialName(blocksField)
|
||||
val blocks: List<InputRichBlock>,
|
||||
@SerialName(hasCheckboxField)
|
||||
val hasCheckbox: Boolean? = null,
|
||||
@SerialName(isCheckedField)
|
||||
val isChecked: Boolean? = null,
|
||||
@SerialName(valueField)
|
||||
val value: Int? = null,
|
||||
/**
|
||||
* For ordered lists, the type of the item label; must be one of "a", "A", "i", "I" or "1".
|
||||
*/
|
||||
@SerialName(typeField)
|
||||
val labelType: String? = null
|
||||
)
|
||||
@@ -0,0 +1,499 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import dev.inmo.tgbotapi.types.animationField
|
||||
import dev.inmo.tgbotapi.types.audioField
|
||||
import dev.inmo.tgbotapi.types.blocksField
|
||||
import dev.inmo.tgbotapi.types.captionField
|
||||
import dev.inmo.tgbotapi.types.cellsField
|
||||
import dev.inmo.tgbotapi.types.creditField
|
||||
import dev.inmo.tgbotapi.types.expressionField
|
||||
import dev.inmo.tgbotapi.types.heightField
|
||||
import dev.inmo.tgbotapi.types.isBorderedField
|
||||
import dev.inmo.tgbotapi.types.isOpenField
|
||||
import dev.inmo.tgbotapi.types.isStripedField
|
||||
import dev.inmo.tgbotapi.types.itemsField
|
||||
import dev.inmo.tgbotapi.types.languageField
|
||||
import dev.inmo.tgbotapi.types.location.StaticLocation
|
||||
import dev.inmo.tgbotapi.types.locationField
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMedia
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaAnimation
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote
|
||||
import dev.inmo.tgbotapi.types.nameField
|
||||
import dev.inmo.tgbotapi.types.photoField
|
||||
import dev.inmo.tgbotapi.types.sizeField
|
||||
import dev.inmo.tgbotapi.types.summaryField
|
||||
import dev.inmo.tgbotapi.types.textField
|
||||
import dev.inmo.tgbotapi.types.typeField
|
||||
import dev.inmo.tgbotapi.types.videoField
|
||||
import dev.inmo.tgbotapi.types.voiceNoteField
|
||||
import dev.inmo.tgbotapi.types.widthField
|
||||
import dev.inmo.tgbotapi.types.zoomField
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* A text paragraph.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockparagraph">InputRichBlockParagraph</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockParagraph(
|
||||
@SerialName(textField)
|
||||
val text: RichText
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "paragraph"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A section heading.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblocksectionheading">InputRichBlockSectionHeading</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockSectionHeading(
|
||||
@SerialName(textField)
|
||||
val text: RichText,
|
||||
/**
|
||||
* Relative size of the text font; 1-6, 1 is the largest, 6 is the smallest.
|
||||
*/
|
||||
@SerialName(sizeField)
|
||||
val level: Int
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "heading"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A preformatted text block.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockpreformatted">InputRichBlockPreformatted</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockPreformatted(
|
||||
@SerialName(textField)
|
||||
val text: RichText,
|
||||
@SerialName(languageField)
|
||||
val language: String? = null
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "pre"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A footer.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockfooter">InputRichBlockFooter</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockFooter(
|
||||
@SerialName(textField)
|
||||
val text: RichText
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "footer"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A divider.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockdivider">InputRichBlockDivider</a>
|
||||
*/
|
||||
@Serializable
|
||||
class InputRichBlockDivider : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
override fun equals(other: Any?): Boolean = other is InputRichBlockDivider
|
||||
|
||||
override fun hashCode(): Int = TYPE.hashCode()
|
||||
|
||||
override fun toString(): String = "InputRichBlockDivider"
|
||||
|
||||
companion object {
|
||||
const val TYPE = "divider"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with a mathematical expression in LaTeX format.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockmathematicalexpression">InputRichBlockMathematicalExpression</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockMathematicalExpression(
|
||||
@SerialName(expressionField)
|
||||
val expression: String
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "mathematical_expression"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with an anchor.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockanchor">InputRichBlockAnchor</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockAnchor(
|
||||
@SerialName(nameField)
|
||||
val name: String
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "anchor"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of blocks.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblocklist">InputRichBlockList</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockList(
|
||||
@SerialName(itemsField)
|
||||
val items: List<InputRichBlockListItem>
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "list"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block quotation.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockblockquotation">InputRichBlockBlockQuotation</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockBlockQuotation(
|
||||
@SerialName(blocksField)
|
||||
val blocks: List<InputRichBlock>,
|
||||
@SerialName(creditField)
|
||||
val credit: RichText? = null
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "blockquote"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A quotation with centered text.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockpullquotation">InputRichBlockPullQuotation</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockPullQuotation(
|
||||
@SerialName(textField)
|
||||
val text: RichText,
|
||||
@SerialName(creditField)
|
||||
val credit: RichText? = null
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "pullquote"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A collage.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockcollage">InputRichBlockCollage</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockCollage(
|
||||
@SerialName(blocksField)
|
||||
val blocks: List<InputRichBlock>,
|
||||
@SerialName(captionField)
|
||||
val caption: RichBlockCaption? = null
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "collage"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A slideshow.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockslideshow">InputRichBlockSlideshow</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockSlideshow(
|
||||
@SerialName(blocksField)
|
||||
val blocks: List<InputRichBlock>,
|
||||
@SerialName(captionField)
|
||||
val caption: RichBlockCaption? = null
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "slideshow"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A table.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblocktable">InputRichBlockTable</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockTable(
|
||||
@SerialName(cellsField)
|
||||
val cells: List<List<RichBlockTableCell>>,
|
||||
@SerialName(isBorderedField)
|
||||
val isBordered: Boolean? = null,
|
||||
@SerialName(isStripedField)
|
||||
val isStriped: Boolean? = null,
|
||||
@SerialName(captionField)
|
||||
val caption: RichText? = null
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "table"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An expandable block for details disclosure.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockdetails">InputRichBlockDetails</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockDetails(
|
||||
@SerialName(summaryField)
|
||||
val summary: RichText,
|
||||
@SerialName(blocksField)
|
||||
val blocks: List<InputRichBlock>,
|
||||
@SerialName(isOpenField)
|
||||
val isOpen: Boolean? = null
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "details"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with a map.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockmap">InputRichBlockMap</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockMap(
|
||||
@SerialName(locationField)
|
||||
val location: StaticLocation,
|
||||
/**
|
||||
* Map zoom level; 0-24.
|
||||
*/
|
||||
@SerialName(zoomField)
|
||||
val zoom: Int,
|
||||
@SerialName(widthField)
|
||||
val width: Int,
|
||||
@SerialName(heightField)
|
||||
val height: Int,
|
||||
@SerialName(captionField)
|
||||
val caption: RichBlockCaption? = null
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "map"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with an animation.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockanimation">InputRichBlockAnimation</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockAnimation(
|
||||
@SerialName(animationField)
|
||||
val animation: TelegramMediaAnimation,
|
||||
@SerialName(captionField)
|
||||
override val caption: RichBlockCaption? = null
|
||||
) : InputRichBlockMedia {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
override val media: TelegramMedia
|
||||
get() = animation
|
||||
|
||||
companion object {
|
||||
const val TYPE = "animation"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with a music file.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockaudio">InputRichBlockAudio</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockAudio(
|
||||
@SerialName(audioField)
|
||||
val audio: TelegramMediaAudio,
|
||||
@SerialName(captionField)
|
||||
override val caption: RichBlockCaption? = null
|
||||
) : InputRichBlockMedia {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
override val media: TelegramMedia
|
||||
get() = audio
|
||||
|
||||
companion object {
|
||||
const val TYPE = "audio"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with a photo.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockphoto">InputRichBlockPhoto</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockPhoto(
|
||||
@SerialName(photoField)
|
||||
val photo: TelegramMediaPhoto,
|
||||
@SerialName(captionField)
|
||||
override val caption: RichBlockCaption? = null
|
||||
) : InputRichBlockMedia {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
override val media: TelegramMedia
|
||||
get() = photo
|
||||
|
||||
companion object {
|
||||
const val TYPE = "photo"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with a video.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockvideo">InputRichBlockVideo</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockVideo(
|
||||
@SerialName(videoField)
|
||||
val video: TelegramMediaVideo,
|
||||
@SerialName(captionField)
|
||||
override val caption: RichBlockCaption? = null
|
||||
) : InputRichBlockMedia {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
override val media: TelegramMedia
|
||||
get() = video
|
||||
|
||||
companion object {
|
||||
const val TYPE = "video"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with a voice note.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockvoicenote">InputRichBlockVoiceNote</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockVoiceNote(
|
||||
@SerialName(voiceNoteField)
|
||||
val voiceNote: TelegramMediaVoiceNote,
|
||||
@SerialName(captionField)
|
||||
override val caption: RichBlockCaption? = null
|
||||
) : InputRichBlockMedia {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
override val media: TelegramMedia
|
||||
get() = voiceNote
|
||||
|
||||
companion object {
|
||||
const val TYPE = "voice_note"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A block with a "Thinking…" placeholder. May be used only in [dev.inmo.tgbotapi.requests.send.SendRichMessageDraft].
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichblockthinking">InputRichBlockThinking</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichBlockThinking(
|
||||
@SerialName(textField)
|
||||
val text: RichText
|
||||
) : InputRichBlock {
|
||||
@EncodeDefault
|
||||
@SerialName(typeField)
|
||||
override val type: String = TYPE
|
||||
|
||||
companion object {
|
||||
const val TYPE = "thinking"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import dev.inmo.tgbotapi.types.location.StaticLocation
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaAnimation
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaAudio
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote
|
||||
|
||||
/**
|
||||
* Builder of [InputRichBlockListItem]s used inside [InputRichBlocksBuilder.list].
|
||||
*/
|
||||
@RichTextDsl
|
||||
class InputRichBlockListBuilder {
|
||||
private val items = mutableListOf<InputRichBlockListItem>()
|
||||
|
||||
fun item(
|
||||
hasCheckbox: Boolean? = null,
|
||||
isChecked: Boolean? = null,
|
||||
value: Int? = null,
|
||||
labelType: String? = null,
|
||||
block: InputRichBlocksBuilder.() -> Unit
|
||||
) {
|
||||
items.add(InputRichBlockListItem(buildInputRichBlocks(block), hasCheckbox, isChecked, value, labelType))
|
||||
}
|
||||
|
||||
fun item(text: String) {
|
||||
items.add(InputRichBlockListItem(listOf(InputRichBlockParagraph(RichTextPlain(text)))))
|
||||
}
|
||||
|
||||
fun build(): List<InputRichBlockListItem> = items.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder of a [List] of [InputRichBlock]s - the root of the rich message input DSL. Text-bearing and container blocks
|
||||
* have their own DSL functions; media blocks (photo, video, animation, audio, voice note, collage, slideshow, table,
|
||||
* map) can be appended with the matching convenience function or with [add] / unary plus.
|
||||
*/
|
||||
@RichTextDsl
|
||||
class InputRichBlocksBuilder {
|
||||
private val blocks = mutableListOf<InputRichBlock>()
|
||||
|
||||
/** Appends an already built [InputRichBlock]. */
|
||||
fun add(block: InputRichBlock) {
|
||||
blocks.add(block)
|
||||
}
|
||||
|
||||
/** Appends an already built [InputRichBlock]. */
|
||||
operator fun InputRichBlock.unaryPlus() = add(this)
|
||||
|
||||
fun paragraph(text: String) = add(InputRichBlockParagraph(RichTextPlain(text)))
|
||||
fun paragraph(block: RichTextBuilder.() -> Unit) = add(InputRichBlockParagraph(buildRichText(block)))
|
||||
|
||||
fun heading(text: String, level: Int) = add(InputRichBlockSectionHeading(RichTextPlain(text), level))
|
||||
fun heading(level: Int, block: RichTextBuilder.() -> Unit) = add(InputRichBlockSectionHeading(buildRichText(block), level))
|
||||
|
||||
fun preformatted(text: String, language: String? = null) = add(InputRichBlockPreformatted(RichTextPlain(text), language))
|
||||
|
||||
fun footer(text: String) = add(InputRichBlockFooter(RichTextPlain(text)))
|
||||
fun footer(block: RichTextBuilder.() -> Unit) = add(InputRichBlockFooter(buildRichText(block)))
|
||||
|
||||
fun divider() = add(InputRichBlockDivider())
|
||||
|
||||
fun mathematicalExpression(expression: String) = add(InputRichBlockMathematicalExpression(expression))
|
||||
|
||||
fun anchor(name: String) = add(InputRichBlockAnchor(name))
|
||||
|
||||
fun thinking(text: String) = add(InputRichBlockThinking(RichTextPlain(text)))
|
||||
fun thinking(block: RichTextBuilder.() -> Unit) = add(InputRichBlockThinking(buildRichText(block)))
|
||||
|
||||
fun list(block: InputRichBlockListBuilder.() -> Unit) = add(InputRichBlockList(InputRichBlockListBuilder().apply(block).build()))
|
||||
|
||||
fun blockQuotation(credit: RichText? = null, block: InputRichBlocksBuilder.() -> Unit) =
|
||||
add(InputRichBlockBlockQuotation(buildInputRichBlocks(block), credit))
|
||||
|
||||
fun pullQuotation(credit: RichText? = null, block: RichTextBuilder.() -> Unit) =
|
||||
add(InputRichBlockPullQuotation(buildRichText(block), credit))
|
||||
|
||||
fun details(summary: RichText, isOpen: Boolean? = null, block: InputRichBlocksBuilder.() -> Unit) =
|
||||
add(InputRichBlockDetails(summary, buildInputRichBlocks(block), isOpen))
|
||||
|
||||
fun details(summary: String, isOpen: Boolean? = null, block: InputRichBlocksBuilder.() -> Unit) =
|
||||
details(RichTextPlain(summary), isOpen, block)
|
||||
|
||||
fun photo(photo: TelegramMediaPhoto, caption: RichBlockCaption? = null) = add(InputRichBlockPhoto(photo, caption))
|
||||
|
||||
fun video(video: TelegramMediaVideo, caption: RichBlockCaption? = null) = add(InputRichBlockVideo(video, caption))
|
||||
|
||||
fun animation(animation: TelegramMediaAnimation, caption: RichBlockCaption? = null) =
|
||||
add(InputRichBlockAnimation(animation, caption))
|
||||
|
||||
fun audio(audio: TelegramMediaAudio, caption: RichBlockCaption? = null) = add(InputRichBlockAudio(audio, caption))
|
||||
|
||||
fun voiceNote(voiceNote: TelegramMediaVoiceNote, caption: RichBlockCaption? = null) =
|
||||
add(InputRichBlockVoiceNote(voiceNote, caption))
|
||||
|
||||
fun collage(caption: RichBlockCaption? = null, block: InputRichBlocksBuilder.() -> Unit) =
|
||||
add(InputRichBlockCollage(buildInputRichBlocks(block), caption))
|
||||
|
||||
fun slideshow(caption: RichBlockCaption? = null, block: InputRichBlocksBuilder.() -> Unit) =
|
||||
add(InputRichBlockSlideshow(buildInputRichBlocks(block), caption))
|
||||
|
||||
fun table(
|
||||
cells: List<List<RichBlockTableCell>>,
|
||||
isBordered: Boolean? = null,
|
||||
isStriped: Boolean? = null,
|
||||
caption: RichText? = null
|
||||
) = add(InputRichBlockTable(cells, isBordered, isStriped, caption))
|
||||
|
||||
fun map(
|
||||
location: StaticLocation,
|
||||
zoom: Int,
|
||||
width: Int,
|
||||
height: Int,
|
||||
caption: RichBlockCaption? = null
|
||||
) = add(InputRichBlockMap(location, zoom, width, height, caption))
|
||||
|
||||
fun build(): List<InputRichBlock> = blocks.toList()
|
||||
}
|
||||
|
||||
/** Builds a [List] of [InputRichBlock]s using the [InputRichBlocksBuilder] DSL. */
|
||||
fun buildInputRichBlocks(block: InputRichBlocksBuilder.() -> Unit): List<InputRichBlock> = InputRichBlocksBuilder().apply(block).build()
|
||||
|
||||
/** Builds an [InputRichMessage] with `blocks` built using the [InputRichBlocksBuilder] DSL. */
|
||||
fun InputRichMessageBlocks(
|
||||
isRtl: Boolean? = null,
|
||||
skipEntityDetection: Boolean? = null,
|
||||
block: InputRichBlocksBuilder.() -> Unit
|
||||
): InputRichMessage = InputRichMessageBlocks(buildInputRichBlocks(block), isRtl, skipEntityDetection)
|
||||
@@ -1,15 +1,21 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
|
||||
import dev.inmo.tgbotapi.types.blocksField
|
||||
import dev.inmo.tgbotapi.types.htmlField
|
||||
import dev.inmo.tgbotapi.types.isRtlField
|
||||
import dev.inmo.tgbotapi.types.markdownField
|
||||
import dev.inmo.tgbotapi.types.media.CoveredTelegramMedia
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMedia
|
||||
import dev.inmo.tgbotapi.types.media.ThumbedTelegramMedia
|
||||
import dev.inmo.tgbotapi.types.mediaField
|
||||
import dev.inmo.tgbotapi.types.skipEntityDetectionField
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Describes a rich message to be sent. Exactly one of the fields [html] or [markdown] must be used. Use the
|
||||
* [InputRichMessageHTML] and [InputRichMessageMarkdown] factories to build an instance.
|
||||
* Describes a rich message to be sent. Exactly one of the fields [html], [markdown] or [blocks] must be used. Use the
|
||||
* [InputRichMessageHTML], [InputRichMessageMarkdown] and [InputRichMessageBlocks] factories to build an instance.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichmessage">InputRichMessage</a>
|
||||
*/
|
||||
@@ -20,14 +26,21 @@ data class InputRichMessage internal constructor(
|
||||
val html: String? = null,
|
||||
@SerialName(markdownField)
|
||||
val markdown: String? = null,
|
||||
@SerialName(blocksField)
|
||||
val blocks: List<InputRichBlock>? = null,
|
||||
@SerialName(mediaField)
|
||||
val media: List<InputRichMessageMedia>? = null,
|
||||
@SerialName(isRtlField)
|
||||
val isRtl: Boolean? = null,
|
||||
@SerialName(skipEntityDetectionField)
|
||||
val skipEntityDetection: Boolean? = null
|
||||
) {
|
||||
init {
|
||||
require((html == null) != (markdown == null)) {
|
||||
"Exactly one of the fields html or markdown must be used in InputRichMessage"
|
||||
require(listOfNotNull(html, markdown, blocks).size == 1) {
|
||||
"Exactly one of the fields html, markdown or blocks must be used in InputRichMessage"
|
||||
}
|
||||
require(media == null || blocks == null) {
|
||||
"The field media of InputRichMessage can be used only together with html or markdown, not blocks"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,10 +53,13 @@ data class InputRichMessage internal constructor(
|
||||
fun InputRichMessageHTML(
|
||||
html: String,
|
||||
isRtl: Boolean? = null,
|
||||
skipEntityDetection: Boolean? = null
|
||||
skipEntityDetection: Boolean? = null,
|
||||
media: List<InputRichMessageMedia>? = null
|
||||
): InputRichMessage = InputRichMessage(
|
||||
html = html,
|
||||
markdown = null,
|
||||
blocks = null,
|
||||
media = media,
|
||||
isRtl = isRtl,
|
||||
skipEntityDetection = skipEntityDetection
|
||||
)
|
||||
@@ -56,10 +72,61 @@ fun InputRichMessageHTML(
|
||||
fun InputRichMessageMarkdown(
|
||||
markdown: String,
|
||||
isRtl: Boolean? = null,
|
||||
skipEntityDetection: Boolean? = null
|
||||
skipEntityDetection: Boolean? = null,
|
||||
media: List<InputRichMessageMedia>? = null
|
||||
): InputRichMessage = InputRichMessage(
|
||||
html = null,
|
||||
markdown = markdown,
|
||||
blocks = null,
|
||||
media = media,
|
||||
isRtl = isRtl,
|
||||
skipEntityDetection = skipEntityDetection
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates an [InputRichMessage] with the content described using a list of [InputRichBlock]s.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichmessage">InputRichMessage</a>
|
||||
*/
|
||||
fun InputRichMessageBlocks(
|
||||
blocks: List<InputRichBlock>,
|
||||
isRtl: Boolean? = null,
|
||||
skipEntityDetection: Boolean? = null
|
||||
): InputRichMessage = InputRichMessage(
|
||||
html = null,
|
||||
markdown = null,
|
||||
blocks = blocks,
|
||||
media = null,
|
||||
isRtl = isRtl,
|
||||
skipEntityDetection = skipEntityDetection
|
||||
)
|
||||
|
||||
/**
|
||||
* Files ([MultipartFile]) referenced by this [InputRichMessage]: the standalone [InputRichMessage.media] entries and any
|
||||
* media block nested (recursively) in [InputRichMessage.blocks], keyed by [MultipartFile.fileId].
|
||||
*/
|
||||
internal val InputRichMessage.multipartFiles: Map<String, MultipartFile>
|
||||
get() {
|
||||
val mediaFiles = media.orEmpty().flatMap { it.media.multipartFiles() }
|
||||
val blockFiles = blocks.orEmpty().flatMap { it.multipartFiles() }
|
||||
return (mediaFiles + blockFiles).associateBy { it.fileId }
|
||||
}
|
||||
|
||||
private fun TelegramMedia.multipartFiles(): List<MultipartFile> = listOfNotNull(
|
||||
file as? MultipartFile,
|
||||
if (this is ThumbedTelegramMedia) {
|
||||
thumb as? MultipartFile
|
||||
} else {
|
||||
null
|
||||
},
|
||||
if (this is CoveredTelegramMedia) {
|
||||
cover as? MultipartFile
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
|
||||
private fun InputRichBlock.multipartFiles(): List<MultipartFile> {
|
||||
val ownFiles = (this as? InputRichBlockMedia) ?.media ?.multipartFiles().orEmpty()
|
||||
return ownFiles + subBlocks.flatMap { it.multipartFiles() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import dev.inmo.tgbotapi.types.idField
|
||||
import dev.inmo.tgbotapi.types.media.RichMessageMemberTelegramMedia
|
||||
import dev.inmo.tgbotapi.types.mediaField
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
private val inputRichMessageMediaIdRegex = Regex("[A-Za-z0-9_-]{1,64}")
|
||||
|
||||
/**
|
||||
* Describes a media referenced from [InputRichMessage.html]/[InputRichMessage.markdown] via `tg://photo?id=`,
|
||||
* `tg://video?id=` and `tg://audio?id=` links.
|
||||
*
|
||||
* @see <a href="https://core.telegram.org/bots/api#inputrichmessagemedia">InputRichMessageMedia</a>
|
||||
*/
|
||||
@Serializable
|
||||
data class InputRichMessageMedia(
|
||||
@SerialName(idField)
|
||||
val id: String,
|
||||
@SerialName(mediaField)
|
||||
val media: RichMessageMemberTelegramMedia
|
||||
) {
|
||||
init {
|
||||
require(id.matches(inputRichMessageMediaIdRegex)) {
|
||||
"id of InputRichMessageMedia must be 1-64 characters long and contain only latin letters, digits, underscores and hyphens, but was \"$id\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import dev.inmo.tgbotapi.requests.abstracts.FileId
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class InputRichBlocksDslTest {
|
||||
@Test
|
||||
fun buildsTextAndContainerBlocks() {
|
||||
val blocks = buildInputRichBlocks {
|
||||
heading("Title", 1)
|
||||
paragraph {
|
||||
plain("Hello ")
|
||||
bold("world")
|
||||
}
|
||||
divider()
|
||||
list {
|
||||
item("first")
|
||||
item(labelType = "1") { paragraph("second") }
|
||||
}
|
||||
blockQuotation {
|
||||
paragraph("quoted")
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(5, blocks.size)
|
||||
assertEquals(InputRichBlockSectionHeading(RichTextPlain("Title"), 1), blocks[0])
|
||||
assertEquals(
|
||||
InputRichBlockParagraph(RichTextGroup(listOf(RichTextPlain("Hello "), RichTextBold(RichTextPlain("world"))))),
|
||||
blocks[1]
|
||||
)
|
||||
assertEquals(InputRichBlockDivider(), blocks[2])
|
||||
|
||||
val list = blocks[3] as InputRichBlockList
|
||||
assertEquals(2, list.items.size)
|
||||
assertEquals(InputRichBlockListItem(listOf(InputRichBlockParagraph(RichTextPlain("first")))), list.items[0])
|
||||
assertEquals("1", list.items[1].labelType)
|
||||
|
||||
assertEquals(InputRichBlockBlockQuotation(listOf(InputRichBlockParagraph(RichTextPlain("quoted")))), blocks[4])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildsMediaBlocks() {
|
||||
val photo = TelegramMediaPhoto(FileId("photo_file_id"))
|
||||
val blocks = buildInputRichBlocks {
|
||||
photo(photo)
|
||||
}
|
||||
assertEquals(listOf(InputRichBlockPhoto(photo)), blocks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildsInputRichMessageBlocks() {
|
||||
val message = InputRichMessageBlocks(isRtl = true) {
|
||||
paragraph("p")
|
||||
}
|
||||
assertEquals(InputRichMessageBlocks(listOf(InputRichBlockParagraph(RichTextPlain("p"))), true), message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package dev.inmo.tgbotapi.types.rich
|
||||
|
||||
import dev.inmo.tgbotapi.requests.abstracts.FileId
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaPhoto
|
||||
import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class InputRichMessageSerializationTest {
|
||||
private val json = Json { encodeDefaults = true }
|
||||
|
||||
@Test
|
||||
fun encodesTextAndContainerBlocks() {
|
||||
val message = InputRichMessageBlocks(
|
||||
blocks = listOf(
|
||||
InputRichBlockParagraph(RichTextPlain("Hello")),
|
||||
InputRichBlockSectionHeading(RichTextPlain("Title"), 2),
|
||||
InputRichBlockDivider(),
|
||||
InputRichBlockList(
|
||||
listOf(
|
||||
InputRichBlockListItem(
|
||||
listOf(InputRichBlockParagraph(RichTextPlain("first"))),
|
||||
labelType = "1"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val blocksJson = json.encodeToJsonElement(InputRichMessage.serializer(), message).jsonObject["blocks"]!!.jsonArray
|
||||
|
||||
val paragraph = blocksJson[0].jsonObject
|
||||
assertEquals("paragraph", paragraph["type"]?.jsonPrimitive?.content)
|
||||
|
||||
val heading = blocksJson[1].jsonObject
|
||||
assertEquals("heading", heading["type"]?.jsonPrimitive?.content)
|
||||
assertEquals(2, heading["size"]?.jsonPrimitive?.int)
|
||||
|
||||
val divider = blocksJson[2].jsonObject
|
||||
assertEquals("divider", divider["type"]?.jsonPrimitive?.content)
|
||||
assertEquals(1, divider.size)
|
||||
|
||||
val list = blocksJson[3].jsonObject
|
||||
assertEquals("list", list["type"]?.jsonPrimitive?.content)
|
||||
val item = list["items"]!!.jsonArray[0].jsonObject
|
||||
assertTrue("label" !in item)
|
||||
assertEquals("1", item["type"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodesTableReusingReceivedCellType() {
|
||||
val table = InputRichBlockTable(
|
||||
cells = listOf(
|
||||
listOf(RichBlockTableCell(RichTextPlain("h1"), isHeader = true, align = "left", valign = "top"))
|
||||
)
|
||||
)
|
||||
val message = InputRichMessageBlocks(blocks = listOf(table))
|
||||
|
||||
val tableJson = json.encodeToJsonElement(InputRichMessage.serializer(), message).jsonObject["blocks"]!!.jsonArray[0].jsonObject
|
||||
val cell = tableJson["cells"]!!.jsonArray[0].jsonArray[0].jsonObject
|
||||
assertEquals("left", cell["align"]?.jsonPrimitive?.content)
|
||||
assertEquals(true, cell["is_header"]?.jsonPrimitive?.boolean)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodesPhotoBlockMedia() {
|
||||
val photoBlock = InputRichBlockPhoto(TelegramMediaPhoto(FileId("photo_file_id")))
|
||||
val message = InputRichMessageBlocks(blocks = listOf(photoBlock))
|
||||
|
||||
val blockJson = json.encodeToJsonElement(InputRichMessage.serializer(), message).jsonObject["blocks"]!!.jsonArray[0].jsonObject
|
||||
assertEquals("photo", blockJson["type"]?.jsonPrimitive?.content)
|
||||
val photoJson = blockJson["photo"]!!.jsonObject
|
||||
assertEquals("photo", photoJson["type"]?.jsonPrimitive?.content)
|
||||
assertEquals("photo_file_id", photoJson["media"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodesVoiceNoteBlock() {
|
||||
val voiceBlock = InputRichBlockVoiceNote(TelegramMediaVoiceNote(FileId("voice_file_id"), duration = 12))
|
||||
val message = InputRichMessageBlocks(blocks = listOf(voiceBlock))
|
||||
|
||||
val blockJson = json.encodeToJsonElement(InputRichMessage.serializer(), message).jsonObject["blocks"]!!.jsonArray[0].jsonObject
|
||||
assertEquals("voice_note", blockJson["type"]?.jsonPrimitive?.content)
|
||||
val voiceJson = blockJson["voice_note"]!!.jsonObject
|
||||
assertEquals("voice_note", voiceJson["type"]?.jsonPrimitive?.content)
|
||||
assertEquals("voice_file_id", voiceJson["media"]?.jsonPrimitive?.content)
|
||||
assertEquals(12, voiceJson["duration"]?.jsonPrimitive?.int)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodesMarkdownWithMedia() {
|
||||
val message = InputRichMessageMarkdown(
|
||||
markdown = "See ",
|
||||
media = listOf(InputRichMessageMedia("abc", TelegramMediaPhoto(FileId("photo_file_id"))))
|
||||
)
|
||||
|
||||
val element = json.encodeToJsonElement(InputRichMessage.serializer(), message).jsonObject
|
||||
assertEquals("See ", element["markdown"]?.jsonPrimitive?.content)
|
||||
val mediaJson = element["media"]!!.jsonArray[0].jsonObject
|
||||
assertEquals("abc", mediaJson["id"]?.jsonPrimitive?.content)
|
||||
assertEquals("photo_file_id", mediaJson["media"]!!.jsonObject["media"]?.jsonPrimitive?.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun roundTripsListOfInputRichBlocks() {
|
||||
val blocks: List<InputRichBlock> = listOf(
|
||||
InputRichBlockParagraph(RichTextGroup(listOf(RichTextPlain("a "), RichTextBold(RichTextPlain("b"))))),
|
||||
InputRichBlockDivider(),
|
||||
InputRichBlockList(
|
||||
listOf(
|
||||
InputRichBlockListItem(
|
||||
listOf(InputRichBlockParagraph(RichTextPlain("item"))),
|
||||
hasCheckbox = true,
|
||||
isChecked = true
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val serializer = ListSerializer(InputRichBlockSerializer)
|
||||
val encoded = json.encodeToString(serializer, blocks)
|
||||
val decoded = json.decodeFromString(serializer, encoded)
|
||||
assertEquals(blocks, decoded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requiresExactlyOneOfHtmlMarkdownOrBlocks() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
InputRichMessage(html = null, markdown = null, blocks = null)
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
InputRichMessage(html = "<b>hi</b>", markdown = "**hi**", blocks = null)
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
InputRichMessage(
|
||||
html = "<b>hi</b>",
|
||||
markdown = null,
|
||||
blocks = listOf(InputRichBlockParagraph(RichTextPlain("hi")))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requiresMediaOnlyWithoutBlocks() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
InputRichMessage(
|
||||
html = null,
|
||||
markdown = null,
|
||||
blocks = listOf(InputRichBlockParagraph(RichTextPlain("hi"))),
|
||||
media = listOf(InputRichMessageMedia("abc", TelegramMediaPhoto(FileId("photo_file_id"))))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun requiresValidMediaId() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
InputRichMessageMedia("", TelegramMediaPhoto(FileId("photo_file_id")))
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
InputRichMessageMedia("has space", TelegramMediaPhoto(FileId("photo_file_id")))
|
||||
}
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
InputRichMessageMedia("x".repeat(65), TelegramMediaPhoto(FileId("photo_file_id")))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user