1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2026-09-03 06:39:13 +00:00

improvements

This commit is contained in:
2026-08-16 23:42:11 +06:00
parent b913f66b1e
commit f9c634ca46
7 changed files with 432 additions and 74 deletions

View File

@@ -5,27 +5,100 @@ 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.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* An item of an [InputRichBlockList].
* An item of an [InputRichBlockList]. Ordered items have a non-null [InputRichBlockListItem.Ordered.value] and
* [InputRichBlockListItem.Ordered.labelType]; unordered items omit both fields from the Bot API payload.
*
* @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
)
@Serializable(InputRichBlockListItem.Serializer::class)
sealed interface InputRichBlockListItem {
val blocks: List<InputRichBlock>
val hasCheckbox: Boolean?
val isChecked: Boolean?
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data class Ordered(
@SerialName(blocksField)
override val blocks: List<InputRichBlock>,
@SerialName(valueField)
val value: Int,
@SerialName(typeField)
val labelType: LabelType = LabelType.Decimals,
@SerialName(hasCheckboxField)
override val hasCheckbox: Boolean? = null,
@SerialName(isCheckedField)
override val isChecked: Boolean? = null
) : InputRichBlockListItem
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data class Unordered(
@SerialName(blocksField)
override val blocks: List<InputRichBlock>,
@SerialName(hasCheckboxField)
override val hasCheckbox: Boolean? = null,
@SerialName(isCheckedField)
override val isChecked: Boolean? = null
) : InputRichBlockListItem
object Serializer : KSerializer<InputRichBlockListItem> {
@Serializable
private data class Surrogate(
@SerialName(blocksField)
val blocks: List<InputRichBlock>,
@SerialName(hasCheckboxField)
val hasCheckbox: Boolean? = null,
@SerialName(isCheckedField)
val isChecked: Boolean? = null,
@SerialName(valueField)
val value: Int? = null,
@SerialName(typeField)
val labelType: LabelType? = null
)
override val descriptor: SerialDescriptor = Surrogate.serializer().descriptor
override fun deserialize(decoder: Decoder): InputRichBlockListItem {
val surrogate = decoder.decodeSerializableValue(Surrogate.serializer())
return when {
surrogate.value != null && surrogate.labelType != null -> Ordered(
surrogate.blocks,
surrogate.value,
surrogate.labelType,
surrogate.hasCheckbox,
surrogate.isChecked
)
surrogate.value == null && surrogate.labelType == null -> Unordered(
surrogate.blocks,
surrogate.hasCheckbox,
surrogate.isChecked
)
else -> throw SerializationException("Ordered InputRichBlockListItem requires both value and type")
}
}
override fun serialize(encoder: Encoder, value: InputRichBlockListItem) {
val surrogate = when (value) {
is Ordered -> Surrogate(
value.blocks,
value.hasCheckbox,
value.isChecked,
value.value,
value.labelType
)
is Unordered -> Surrogate(value.blocks, value.hasCheckbox, value.isChecked)
}
encoder.encodeSerializableValue(Surrogate.serializer(), surrogate)
}
}
}

View File

@@ -8,27 +8,45 @@ import dev.inmo.tgbotapi.types.media.TelegramMediaVideo
import dev.inmo.tgbotapi.types.media.TelegramMediaVoiceNote
/**
* Builder of [InputRichBlockListItem]s used inside [InputRichBlocksBuilder.list].
* Builder of [InputRichBlockListItem.Ordered] items used inside [InputRichBlocksBuilder.orderedList].
*/
@RichTextDsl
class InputRichBlockListBuilder {
private val items = mutableListOf<InputRichBlockListItem>()
class InputRichBlockOrderedListBuilder {
private val items = mutableListOf<InputRichBlockListItem.Ordered>()
fun item(
value: Int,
labelType: LabelType = LabelType.Decimals,
hasCheckbox: Boolean? = null,
isChecked: Boolean? = null,
block: InputRichBlocksBuilder.() -> Unit
) {
items.add(InputRichBlockListItem.Ordered(buildInputRichBlocks(block), value, labelType, hasCheckbox, isChecked))
}
fun build(): List<InputRichBlockListItem.Ordered> = items.toList()
}
/**
* Builder of [InputRichBlockListItem.Unordered] items used inside [InputRichBlocksBuilder.unorderedList].
*/
@RichTextDsl
class InputRichBlockUnorderedListBuilder {
private val items = mutableListOf<InputRichBlockListItem.Unordered>()
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))
items.add(InputRichBlockListItem.Unordered(buildInputRichBlocks(block), hasCheckbox, isChecked))
}
fun item(text: String) {
items.add(InputRichBlockListItem(listOf(InputRichBlockParagraph(RichTextPlain(text)))))
items.add(InputRichBlockListItem.Unordered(listOf(InputRichBlockParagraph(RichTextPlain(text)))))
}
fun build(): List<InputRichBlockListItem> = items.toList()
fun build(): List<InputRichBlockListItem.Unordered> = items.toList()
}
/**
@@ -54,6 +72,19 @@ class InputRichBlocksBuilder {
fun heading(text: String, level: Int) = add(InputRichBlockSectionHeading(RichTextPlain(text), level))
fun heading(level: Int, block: RichTextBuilder.() -> Unit) = add(InputRichBlockSectionHeading(buildRichText(block), level))
fun h1(text: String) = heading(text, 1)
fun h1(block: RichTextBuilder.() -> Unit) = heading(1, block)
fun h2(text: String) = heading(text, 2)
fun h2(block: RichTextBuilder.() -> Unit) = heading(2, block)
fun h3(text: String) = heading(text, 3)
fun h3(block: RichTextBuilder.() -> Unit) = heading(3, block)
fun h4(text: String) = heading(text, 4)
fun h4(block: RichTextBuilder.() -> Unit) = heading(4, block)
fun h5(text: String) = heading(text, 5)
fun h5(block: RichTextBuilder.() -> Unit) = heading(5, block)
fun h6(text: String) = heading(text, 6)
fun h6(block: RichTextBuilder.() -> Unit) = heading(6, block)
fun preformatted(text: String, language: String? = null) = add(InputRichBlockPreformatted(RichTextPlain(text), language))
fun footer(text: String) = add(InputRichBlockFooter(RichTextPlain(text)))
@@ -68,7 +99,11 @@ class InputRichBlocksBuilder {
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 orderedList(block: InputRichBlockOrderedListBuilder.() -> Unit) =
add(InputRichBlockList(InputRichBlockOrderedListBuilder().apply(block).build()))
fun unorderedList(block: InputRichBlockUnorderedListBuilder.() -> Unit) =
add(InputRichBlockList(InputRichBlockUnorderedListBuilder().apply(block).build()))
fun blockQuotation(credit: RichText? = null, block: InputRichBlocksBuilder.() -> Unit) =
add(InputRichBlockBlockQuotation(buildInputRichBlocks(block), credit))

View File

@@ -0,0 +1,65 @@
package dev.inmo.tgbotapi.types.rich
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* Type of an ordered [InputRichBlockListItem] label.
*/
@Serializable(LabelType.Serializer::class)
sealed interface LabelType {
val typeSymbol: String
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data object LettersUppercase : LabelType {
override val typeSymbol: String = "A"
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data object LettersLowercase : LabelType {
override val typeSymbol: String = "a"
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data object RomanUppercase : LabelType {
override val typeSymbol: String = "I"
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data object RomanLowercase : LabelType {
override val typeSymbol: String = "i"
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Serializer::class)
data object Decimals : LabelType {
override val typeSymbol: String = "1"
}
object Serializer : KSerializer<LabelType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LabelType", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): LabelType = when (val typeSymbol = decoder.decodeString()) {
LettersUppercase.typeSymbol -> LettersUppercase
LettersLowercase.typeSymbol -> LettersLowercase
RomanUppercase.typeSymbol -> RomanUppercase
RomanLowercase.typeSymbol -> RomanLowercase
Decimals.typeSymbol -> Decimals
else -> throw SerializationException("Unknown InputRichBlockListItem label type: $typeSymbol")
}
override fun serialize(encoder: Encoder, value: LabelType) {
encoder.encodeString(value.typeSymbol)
}
}
}

View File

@@ -15,16 +15,19 @@ class InputRichBlocksDslTest {
bold("world")
}
divider()
list {
unorderedList {
item("first")
item(labelType = "1") { paragraph("second") }
item { paragraph("second") }
}
orderedList {
item(value = 2) { paragraph("third") }
}
blockQuotation {
paragraph("quoted")
}
}
assertEquals(5, blocks.size)
assertEquals(6, blocks.size)
assertEquals(InputRichBlockSectionHeading(RichTextPlain("Title"), 1), blocks[0])
assertEquals(
InputRichBlockParagraph(RichTextGroup(listOf(RichTextPlain("Hello "), RichTextBold(RichTextPlain("world"))))),
@@ -34,10 +37,13 @@ class InputRichBlocksDslTest {
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(InputRichBlockListItem.Unordered(listOf(InputRichBlockParagraph(RichTextPlain("first")))), list.items[0])
assertEquals(InputRichBlockListItem.Unordered::class, list.items[1]::class)
assertEquals(InputRichBlockBlockQuotation(listOf(InputRichBlockParagraph(RichTextPlain("quoted")))), blocks[4])
val orderedList = blocks[4] as InputRichBlockList
assertEquals(LabelType.Decimals, (orderedList.items.single() as InputRichBlockListItem.Ordered).labelType)
assertEquals(InputRichBlockBlockQuotation(listOf(InputRichBlockParagraph(RichTextPlain("quoted")))), blocks[5])
}
@Test
@@ -56,4 +62,20 @@ class InputRichBlocksDslTest {
}
assertEquals(InputRichMessageBlocks(listOf(InputRichBlockParagraph(RichTextPlain("p"))), true), message)
}
@Test
fun buildsHeadingShortcuts() {
val blocks = buildInputRichBlocks {
h1("h1")
h2 { bold("h2") }
h3("h3")
h4 { plain("h4") }
h5("h5")
h6 { italic("h6") }
}
assertEquals((1..6).toList(), blocks.map { (it as InputRichBlockSectionHeading).level })
assertEquals(RichTextPlain("h1"), (blocks[0] as InputRichBlockSectionHeading).text)
assertEquals(RichTextBold(RichTextPlain("h2")), (blocks[1] as InputRichBlockSectionHeading).text)
}
}

View File

@@ -4,6 +4,7 @@ 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.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@@ -27,9 +28,9 @@ class InputRichMessageSerializationTest {
InputRichBlockDivider(),
InputRichBlockList(
listOf(
InputRichBlockListItem(
InputRichBlockListItem.Ordered(
listOf(InputRichBlockParagraph(RichTextPlain("first"))),
labelType = "1"
value = 1
)
)
)
@@ -117,7 +118,7 @@ class InputRichMessageSerializationTest {
InputRichBlockDivider(),
InputRichBlockList(
listOf(
InputRichBlockListItem(
InputRichBlockListItem.Unordered(
listOf(InputRichBlockParagraph(RichTextPlain("item"))),
hasCheckbox = true,
isChecked = true
@@ -132,6 +133,52 @@ class InputRichMessageSerializationTest {
assertEquals(blocks, decoded)
}
@Test
fun serializesAndDeserializesListItemVariantsWithFlatShape() {
val ordered: InputRichBlockListItem = InputRichBlockListItem.Ordered(
blocks = emptyList(),
value = 4,
labelType = LabelType.RomanUppercase
)
val unordered: InputRichBlockListItem = InputRichBlockListItem.Unordered(blocks = emptyList())
val orderedElement = json.encodeToJsonElement(InputRichBlockListItem.Serializer, ordered).jsonObject
assertEquals(4, orderedElement["value"]?.jsonPrimitive?.int)
assertEquals("I", orderedElement["type"]?.jsonPrimitive?.content)
assertEquals(ordered, json.decodeFromJsonElement(InputRichBlockListItem.Serializer, orderedElement))
assertEquals(unordered, json.decodeFromString(InputRichBlockListItem.Serializer, "{\"blocks\":[]}"))
}
@Test
fun serializesEveryLabelTypeAsItsSymbol() {
val labelTypes = listOf(
LabelType.LettersUppercase,
LabelType.LettersLowercase,
LabelType.RomanUppercase,
LabelType.RomanLowercase,
LabelType.Decimals
)
labelTypes.forEach { labelType ->
val encoded = json.encodeToString(LabelType.Serializer, labelType)
assertEquals("\"${labelType.typeSymbol}\"", encoded)
assertEquals(labelType, json.decodeFromString(LabelType.Serializer, encoded))
}
}
@Test
fun rejectsIncompleteOrderedListItemsAndUnknownLabelTypes() {
assertFailsWith<SerializationException> {
json.decodeFromString(InputRichBlockListItem.Serializer, "{\"blocks\":[],\"value\":1}")
}
assertFailsWith<SerializationException> {
json.decodeFromString(InputRichBlockListItem.Serializer, "{\"blocks\":[],\"type\":\"1\"}")
}
assertFailsWith<SerializationException> {
json.decodeFromString(LabelType.Serializer, "\"?\"")
}
}
@Test
fun requiresExactlyOneOfHtmlMarkdownOrBlocks() {
assertFailsWith<IllegalArgumentException> {