From e403bbca0bb4398499aef1343a7532437e3cc8ad Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 28 May 2024 23:46:30 +0600 Subject: [PATCH] add support of expandable blockquote --- .../types/message/RawMessageEntity.kt | 3 ++ .../types/message/content/AnimationContent.kt | 29 +++++++------- .../types/message/content/PhotoContent.kt | 21 +++++----- .../types/message/content/VideoContent.kt | 31 ++++++++------- .../ExpandableBlockquoteTextSource.kt | 26 +++++++++++++ .../types/message/textsources/TextSource.kt | 39 +++++++++++++++---- .../inmo/tgbotapi/utils/EntitiesBuilder.kt | 38 ++++++++++++++++++ .../MultilevelTextSourceFormatting.kt | 11 ++++++ .../utils/internal/StringFormatting.kt | 5 +++ .../types/MessageEntity/EntitiesTestText.kt | 18 ++++++++- .../MessageEntity/StringFormattingTests.kt | 6 +++ 11 files changed, 178 insertions(+), 49 deletions(-) create mode 100644 tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/ExpandableBlockquoteTextSource.kt diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt index 38058eb101..bf14be55b5 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessageEntity.kt @@ -32,6 +32,7 @@ internal data class RawMessageEntity( "phone_number" -> 1 "bold" -> 1 "blockquote" -> 0 + "expandable_blockquote" -> 0 "italic" -> 1 "text_mention" -> 1 "strikethrough" -> 1 @@ -66,6 +67,7 @@ internal fun RawMessageEntity.asTextSource( "phone_number" -> PhoneNumberTextSource(sourceSubstring, subPartsWithRegulars) "bold" -> BoldTextSource(sourceSubstring, subPartsWithRegulars) "blockquote" -> BlockquoteTextSource(sourceSubstring, subPartsWithRegulars) + "expandable_blockquote" -> ExpandableBlockquoteTextSource(sourceSubstring, subPartsWithRegulars) "italic" -> ItalicTextSource(sourceSubstring, subPartsWithRegulars) "code" -> CodeTextSource(sourceSubstring) "pre" -> PreTextSource(sourceSubstring, language) @@ -186,6 +188,7 @@ internal fun TextSource.toRawMessageEntities(offset: Int = 0): List RawMessageEntity("phone_number", offset, length) is BoldTextSource -> RawMessageEntity("bold", offset, length) is BlockquoteTextSource -> RawMessageEntity("blockquote", offset, length) + is ExpandableBlockquoteTextSource -> RawMessageEntity("expandable_blockquote", offset, length) is ItalicTextSource -> RawMessageEntity("italic", offset, length) is CodeTextSource -> RawMessageEntity("code", offset, length) is PreTextSource -> RawMessageEntity("pre", offset, length, language = language) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/AnimationContent.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/AnimationContent.kt index 1aa583143e..02eb935111 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/AnimationContent.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/AnimationContent.kt @@ -31,20 +31,21 @@ data class AnimationContent( replyParameters: ReplyParameters?, replyMarkup: KeyboardMarkup? ): Request> = SendAnimation( - chatId, - media.fileId, - media.thumbnail ?.fileId, - textSources, - spoilered, - media.duration, - media.width, - media.height, - messageThreadId, - businessConnectionId, - disableNotification, - protectContent, - replyParameters, - replyMarkup + chatId = chatId, + animation = media.fileId, + thumbnail = media.thumbnail ?.fileId, + entities = textSources, + showCaptionAboveMedia = showCaptionAboveMedia, + spoilered = spoilered, + duration = media.duration, + width = media.width, + height = media.height, + threadId = messageThreadId, + businessConnectionId = businessConnectionId, + disableNotification = disableNotification, + protectContent = protectContent, + replyParameters = replyParameters, + replyMarkup = replyMarkup ) override fun asTelegramMedia(): TelegramMediaAnimation = TelegramMediaAnimation( diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/PhotoContent.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/PhotoContent.kt index 2b0b643f00..f971f7fb4e 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/PhotoContent.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/PhotoContent.kt @@ -33,16 +33,17 @@ data class PhotoContent( replyParameters: ReplyParameters?, replyMarkup: KeyboardMarkup? ): Request> = SendPhoto( - chatId, - media.fileId, - textSources, - spoilered, - messageThreadId, - businessConnectionId, - disableNotification, - protectContent, - replyParameters, - replyMarkup + chatId = chatId, + photo = media.fileId, + entities = textSources, + showCaptionAboveMedia = showCaptionAboveMedia, + spoilered = spoilered, + threadId = messageThreadId, + businessConnectionId = businessConnectionId, + disableNotification = disableNotification, + protectContent = protectContent, + replyParameters = replyParameters, + replyMarkup = replyMarkup ) override fun toMediaGroupMemberTelegramMedia(): TelegramMediaPhoto = asTelegramMedia() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/VideoContent.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/VideoContent.kt index 08d72e5b17..90b1062159 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/VideoContent.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/VideoContent.kt @@ -30,21 +30,22 @@ data class VideoContent( replyParameters: ReplyParameters?, replyMarkup: KeyboardMarkup? ): Request> = SendVideo( - chatId, - media.fileId, - media.thumbnail ?.fileId, - textSources, - spoilered, - media.duration, - media.width, - media.height, - null, - messageThreadId, - businessConnectionId, - disableNotification, - protectContent, - replyParameters, - replyMarkup + chatId = chatId, + video = media.fileId, + thumbnail = media.thumbnail ?.fileId, + entities = textSources, + showCaptionAboveMedia = showCaptionAboveMedia, + spoilered = spoilered, + duration = media.duration, + width = media.width, + height = media.height, + supportStreaming = null, + threadId = messageThreadId, + businessConnectionId = businessConnectionId, + disableNotification = disableNotification, + protectContent = protectContent, + replyParameters = replyParameters, + replyMarkup = replyMarkup ) override fun toMediaGroupMemberTelegramMedia(): TelegramMediaVideo = asTelegramMedia() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/ExpandableBlockquoteTextSource.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/ExpandableBlockquoteTextSource.kt new file mode 100644 index 0000000000..50730463e1 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/ExpandableBlockquoteTextSource.kt @@ -0,0 +1,26 @@ +package dev.inmo.tgbotapi.types.message.textsources + +import dev.inmo.tgbotapi.utils.RiskFeature +import dev.inmo.tgbotapi.utils.extensions.makeString +import dev.inmo.tgbotapi.utils.internal.* +import kotlinx.serialization.Serializable + +/** + * @see expandableBlockquote + */ +@Serializable +data class ExpandableBlockquoteTextSource @RiskFeature(DirectInvocationOfTextSourceConstructor) constructor ( + override val source: String, + override val subsources: TextSourcesList +) : MultilevelTextSource { + override val markdown: String by lazy { source.expandableBlockquoteMarkdown() } + override val markdownV2: String by lazy { expandableBlockquoteMarkdownV2() } + override val html: String by lazy { expandableBlockquoteHTML() } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun expandableBlockquote(parts: TextSourcesList) = ExpandableBlockquoteTextSource(parts.makeString(), parts) +@Suppress("NOTHING_TO_INLINE") +inline fun expandableBlockquote(vararg parts: TextSource) = expandableBlockquote(parts.toList()) +@Suppress("NOTHING_TO_INLINE") +inline fun expandableBlockquote(text: String) = expandableBlockquote(regular(text)) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/TextSource.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/TextSource.kt index 4a44363d69..e2ee14a630 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/TextSource.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/TextSource.kt @@ -22,14 +22,37 @@ sealed interface TextSource { get() = source } -@Suppress("NOTHING_TO_INLINE") -inline operator fun TextSource.plus(other: TextSource) = listOf(this, other) -@Suppress("NOTHING_TO_INLINE") -inline operator fun TextSource.plus(other: List) = listOf(this) + other -@Suppress("NOTHING_TO_INLINE") -inline operator fun TextSource.plus(text: String) = listOf(this, regular(text)) -@Suppress("NOTHING_TO_INLINE") -inline operator fun List.plus(text: String) = this + regular(text) +operator fun TextSource.plus(other: TextSource) = when { + this is RegularTextSource && other is RegularTextSource -> listOf(RegularTextSource(source + other.source)) + else -> listOf(this, other) +} +operator fun TextSource.plus(text: String) = this + regular(text) +operator fun List.plus(text: String): List { + val newList = mutableListOf() + + for (i in 0 until size - 1) { + newList.add(get(i)) + } + + val sublist = lastOrNull() ?.let { + it + text + } ?: listOf(regular(text)) + + newList.addAll(sublist) + + return newList +} +operator fun TextSource.plus(other: List) = other.fold(listOf(this)) { acc, textSource -> + val newList = mutableListOf() + + for (i in 0 until acc.size - 1) { + newList.add(acc.get(i)) + } + + newList.addAll(acc.last() + textSource) + + newList +} @Serializable(TextSourceSerializer::class) sealed interface MultilevelTextSource : TextSource { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/EntitiesBuilder.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/EntitiesBuilder.kt index 60930f0507..7fd8804f4f 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/EntitiesBuilder.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/EntitiesBuilder.kt @@ -140,6 +140,44 @@ inline fun EntitiesBuilder.blockquote(text: String) = add(dev.inmo.tgbotapi.type */ inline fun EntitiesBuilder.blockquoteln(text: String) = blockquote(text) + newLine + +/** + * Add blockquote using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.blockquote] + */ +inline fun EntitiesBuilder.expandableBlockquote(parts: TextSourcesList) = add(dev.inmo.tgbotapi.types.message.textsources.expandableBlockquote(parts)) +/** + * Version of [EntitiesBuilder.expandableBlockquote] with new line at the end + */ +inline fun EntitiesBuilder.expandableBlockquoteln(parts: TextSourcesList) = expandableBlockquote(parts) + newLine +/** + * Add expandableBlockquote using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.expandableBlockquote]. + * Will reuse separator config from [buildEntities] + */ +inline fun EntitiesBuilder.expandableBlockquote(noinline init: EntitiesBuilderBody) = add(dev.inmo.tgbotapi.types.message.textsources.expandableBlockquote( + buildEntities(separator, init) +)) +/** + * Version of [EntitiesBuilder.expandableBlockquote] with new line at the end. + * Will reuse separator config from [buildEntities] + */ +inline fun EntitiesBuilder.expandableBlockquoteln(noinline init: EntitiesBuilderBody) = expandableBlockquote(init) + newLine +/** + * Add expandableBlockquote using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.expandableBlockquote] + */ +inline fun EntitiesBuilder.expandableBlockquote(vararg parts: TextSource) = add(dev.inmo.tgbotapi.types.message.textsources.expandableBlockquote(*parts)) +/** + * Version of [EntitiesBuilder.expandableBlockquote] with new line at the end + */ +inline fun EntitiesBuilder.expandableBlockquoteln(vararg parts: TextSource) = expandableBlockquote(*parts) + newLine +/** + * Add expandableBlockquote using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.expandableBlockquote] + */ +inline fun EntitiesBuilder.expandableBlockquote(text: String) = add(dev.inmo.tgbotapi.types.message.textsources.expandableBlockquote(text)) +/** + * Version of [EntitiesBuilder.expandableBlockquote] with new line at the end + */ +inline fun EntitiesBuilder.expandableBlockquoteln(text: String) = expandableBlockquote(text) + newLine + /** * Add spoiler using [EntitiesBuilder.add] with [dev.inmo.tgbotapi.types.message.textsources.spoiler] */ diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/MultilevelTextSourceFormatting.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/MultilevelTextSourceFormatting.kt index c1dd5c6c67..56c8579e75 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/MultilevelTextSourceFormatting.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/MultilevelTextSourceFormatting.kt @@ -45,6 +45,17 @@ internal fun MultilevelTextSource.blockquoteMarkdownV2(): String = markdownV2Def internal fun MultilevelTextSource.blockquoteHTML(): String = htmlDefault(htmlBlockquoteControl) +internal fun MultilevelTextSource.expandableBlockquoteMarkdownV2(): String = markdownV2Default( + openControlSymbol = markdownV2ExpandableBlockquoteOpenControl, + closeControlSymbol = markdownV2ExpandableBlockquoteCloseControl, + eachLineSeparator = markdownBlockquoteControl +) +internal fun MultilevelTextSource.expandableBlockquoteHTML(): String = htmlDefault( + openControlSymbol = htmlBlockquoteOpenControl, + closeControlSymbol = htmlBlockquoteControl +) + + internal fun MultilevelTextSource.cashTagMarkdownV2(): String = subsources.makeMarkdownV2String() internal fun MultilevelTextSource.cashTagHTML(): String = subsources.makeHtmlString() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/StringFormatting.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/StringFormatting.kt index a8a0e4673a..73d2b7d1d5 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/StringFormatting.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/StringFormatting.kt @@ -16,9 +16,12 @@ const val markdownV2StrikethroughControl = "~" const val markdownV2UnderlineControl = "__" const val markdownV2UnderlineEndControl = "$markdownV2UnderlineControl$markdownV2ItalicUnderlineDelimiter" const val markdownV2ItalicEndControl = "$markdownItalicControl$markdownV2ItalicUnderlineDelimiter" +const val markdownV2ExpandableBlockquoteOpenControl = "**" +const val markdownV2ExpandableBlockquoteCloseControl = "||" const val htmlBoldControl = "b" const val htmlBlockquoteControl = "blockquote" +const val htmlBlockquoteOpenControl = "blockquote expandable" const val htmlItalicControl = "i" const val htmlSpoilerControl = "span class=\"tg-spoiler\"" const val htmlSpoilerClosingControl = "span" @@ -51,6 +54,8 @@ internal fun String.boldMarkdown(): String = markdownDefault(markdownBoldControl internal fun String.blockquoteMarkdown(): String = regularMarkdown() +internal fun String.expandableBlockquoteMarkdown(): String = regularMarkdown() + internal fun String.italicMarkdown(): String = markdownDefault(markdownItalicControl) internal fun String.spoilerMarkdown(): String = regularMarkdown() diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/EntitiesTestText.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/EntitiesTestText.kt index 6bc3340139..776e3b5724 100644 --- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/EntitiesTestText.kt +++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/EntitiesTestText.kt @@ -1,14 +1,21 @@ package dev.inmo.tgbotapi.types.MessageEntity import dev.inmo.tgbotapi.types.message.RawMessageEntity +import dev.inmo.tgbotapi.types.message.asTextSources import dev.inmo.tgbotapi.types.message.textsources.* +import dev.inmo.tgbotapi.types.message.toRawMessageEntities +import dev.inmo.tgbotapi.utils.extensions.makeSourceString +import kotlin.test.assertEquals import kotlin.test.assertTrue const val testText = "It (is?) is simple hello world with #tag and @mention. Start of blockquote: Block quotation started\n" + + "Block quotation continued\n" + + "The last line of the block quotation\n" + + ". Start of expandable blockquote: Block quotation started\n" + "Block quotation continued\n" + "The last line of the block quotation" -const val formattedV2Text = "It \\(is?\\) *_is_ ~__simple__~* ||hello world|| with \\#tag and @mention\\. Start of blockquote: >Block quotation started\n>Block quotation continued\n>The last line of the block quotation" -const val formattedHtmlText = "It (is?) is simple hello world with #tag and @mention. Start of blockquote:
Block quotation started\nBlock quotation continued\nThe last line of the block quotation
" +const val formattedV2Text = "It \\(is?\\) *_is_ ~__simple__~* ||hello world|| with \\#tag and @mention\\. Start of blockquote: >Block quotation started\n>Block quotation continued\n>The last line of the block quotation\n\\. Start of expandable blockquote: **>Block quotation started\n>Block quotation continued\n>The last line of the block quotation||" +const val formattedHtmlText = "It (is?) is simple hello world with #tag and @mention. Start of blockquote:
Block quotation started\nBlock quotation continued\nThe last line of the block quotation
\n. Start of expandable blockquote:
Block quotation started\nBlock quotation continued\nThe last line of the block quotation
" internal val testTextEntities = listOf( RawMessageEntity( "bold", @@ -49,6 +56,11 @@ internal val testTextEntities = listOf( "blockquote", 76, 86 + ), + RawMessageEntity( + "expandable_blockquote", + 120, + 204 ) ) @@ -72,4 +84,6 @@ fun TextSourcesList.testTextSources() { val blockquoteSource = get(9) as BlockquoteTextSource assertTrue (blockquoteSource.subsources.first() is RegularTextSource) + + assertEquals(this, toRawMessageEntities().asTextSources(makeSourceString())) } diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/StringFormattingTests.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/StringFormattingTests.kt index 8077fbede4..d62fe47014 100644 --- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/StringFormattingTests.kt +++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/MessageEntity/StringFormattingTests.kt @@ -54,6 +54,12 @@ class StringFormattingTests { "Block quotation started\n" + "Block quotation continued\n" + "The last line of the block quotation" + ) + + "\n. Start of expandable blockquote: " + + expandableBlockquote( + "Block quotation started\n" + + "Block quotation continued\n" + + "The last line of the block quotation" ) sources.testTextSources()