From f9c634ca462d0c05e6cdf064f25c17b9ae0504b8 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Sun, 16 Aug 2026 23:42:11 +0600 Subject: [PATCH] improvements --- CHANGELOG.md | 5 +- tgbotapi.core/api/tgbotapi.core.api | 189 ++++++++++++++---- .../types/rich/InputRichBlockParts.kt | 107 ++++++++-- .../tgbotapi/types/rich/InputRichBlocksDsl.kt | 53 ++++- .../dev/inmo/tgbotapi/types/rich/LabelType.kt | 65 ++++++ .../types/rich/InputRichBlocksDslTest.kt | 34 +++- .../rich/InputRichMessageSerializationTest.kt | 53 ++++- 7 files changed, 432 insertions(+), 74 deletions(-) create mode 100644 tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/LabelType.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index b8dbdb7ccc..eac13715f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ **Breaking changes**: +* `InputRichBlockListItem` is now a sealed interface with `Ordered` and `Unordered` implementations; ordered items require a non-null `value` and typed `LabelType`, while `LabelType.Decimals` is the default; `InputRichBlocksBuilder` now exposes separate `orderedList` and `unorderedList` DSL functions with matching item builders * `ReplyParameters` is now a sealed interface with nested `ReplyParameters.Chat` and `ReplyParameters.Ephemeral` implementations; overloaded `ReplyParameters(...)` companion factories construct both regular-message and ephemeral-message variants * The 4 group-family message implementations (`CommonGroupContentMessageImpl`, `CommonForumContentMessageImpl`, `CommonChannelDirectMessagesContentMessageImpl`, `CommonSuggestedChannelDirectMessagesContentMessageImpl`) gained trailing `receiverUser`/`ephemeralMessageId` primary-constructor parameters (defaulted to `null`) — positional construction of these classes must be updated * `reply(to = ...)` overloads for the 13 ephemeral-capable senders (see below) now detect an ephemeral `to` target (`to is PossiblyEphemeralMessage && to.ephemeralMessageId != null`) and automatically address the reply through `ephemeral_message_id`, sending the outgoing message itself as ephemeral to the same receiver — this is a behavior change for any existing code that replies to a message carrying a non-null `ephemeralMessageId` @@ -13,11 +14,13 @@ **Migration advice**: Keep `ReplyParameters(...)` for both chat-message and ephemeral-message targets; explicit `ReplyParameters.Ephemeral(...)` subtype construction also remains available; pass `receiverUser`/`ephemeralMessageId` explicitly (or rely on their `null` default) when constructing the 4 group-family message implementations positionally * `Core`: + * (`Rich Messages`) Added `h1` through `h6` shortcuts for plain and rich-text headings in `InputRichBlocksBuilder` + * (`Rich Messages`) Replaced the flat `InputRichBlockListItem` data class with ordered/unordered variants, added the string-serialized `LabelType` hierarchy (`A`, `a`, `I`, `i`, `1`), and split the list DSL into `InputRichBlockOrderedListBuilder`/`InputRichBlockUnorderedListBuilder` * (`Rich Messages`) Added `InputRichBlock` hierarchy with all 21 `InputRichBlock*` types (mirroring the received `RichBlock*` hierarchy and reusing `RichText`/`RichBlockCaption`/`RichBlockTableCell`), the label-less `InputRichBlockListItem` and the `InputRichBlockSerializer`; every `InputRichBlock` exposes `subBlocks` navigation * (`Rich Messages`) Added `TelegramMediaVoiceNote` (`InputMediaVoiceNote`) and the `RichMessageMemberTelegramMedia` marker interface implemented by it, `TelegramMediaAnimation`, `TelegramMediaAudio`, `TelegramMediaPhoto` and `TelegramMediaVideo`; added `VoiceFile.toTelegramMediaVoiceNote` converters * (`Rich Messages`) Added `InputRichMessageMedia` type (media referenced from `InputRichMessage.html`/`.markdown` via `tg://photo?id=`, `tg://video?id=` and `tg://audio?id=` links) * (`Rich Messages`) `InputRichMessage` gained `blocks` and `media` fields ("exactly one of `html`, `markdown` or `blocks` must be used") and the `InputRichMessageBlocks` factory; `InputRichMessageHTML`/`InputRichMessageMarkdown` gained a trailing `media` parameter - * (`Rich Messages`) Added Rich Messages input DSL builders `buildInputRichBlocks`/`InputRichBlocksBuilder`, `InputRichBlockListBuilder` and the `InputRichMessageBlocks { }` builder overload (marked with the `@RichTextDsl` DSL marker) + * (`Rich Messages`) Added Rich Messages input DSL builders `buildInputRichBlocks`/`InputRichBlocksBuilder`, typed ordered/unordered list builders and the `InputRichMessageBlocks { }` builder overload (marked with the `@RichTextDsl` DSL marker) * (`Rich Messages`) `SendRichMessage` now supports `attach://` upload of new files referenced from `InputRichMessage.blocks`/`.media`; `SendRichMessageDraft` now rejects rich messages that require direct file upload (unsupported by the method) * (`Ephemeral Messages`) Added `EphemeralMessageId` value class, `PossiblyEphemeralMessage` (`receiverUser`/`ephemeralMessageId`) and `EphemeralMessageAction` (`receiverUserId`/`ephemeralMessageId`) abstractions * (`Ephemeral Messages`) `RawMessage` parses `receiver_user`/`ephemeral_message_id`; the 4 group-family `Common*ContentMessage` types (`CommonGroupContentMessage`, `CommonForumContentMessage`, `CommonChannelDirectMessagesContentMessage`, `CommonSuggestedChannelDirectMessagesContentMessage`, via the shared `PotentiallyFromUserGroupContentMessage`) now also implement `PossiblyEphemeralMessage` diff --git a/tgbotapi.core/api/tgbotapi.core.api b/tgbotapi.core/api/tgbotapi.core.api index bec4e866d1..15485f1029 100644 --- a/tgbotapi.core/api/tgbotapi.core.api +++ b/tgbotapi.core/api/tgbotapi.core.api @@ -35475,50 +35475,72 @@ public final class dev/inmo/tgbotapi/types/rich/InputRichBlockList$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } -public final class dev/inmo/tgbotapi/types/rich/InputRichBlockListBuilder { - public fun ()V - public final fun build ()Ljava/util/List; - public final fun item (Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public final fun item (Ljava/lang/String;)V - public static synthetic fun item$default (Ldev/inmo/tgbotapi/types/rich/InputRichBlockListBuilder;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V -} - -public final class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem { +public abstract interface class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem { public static final field Companion Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Companion; - public fun (Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;)V - public synthetic fun (Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/util/List; - public final fun component2 ()Ljava/lang/Boolean; - public final fun component3 ()Ljava/lang/Boolean; - public final fun component4 ()Ljava/lang/Integer; - public final fun component5 ()Ljava/lang/String; - public final fun copy (Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;)Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem; - public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/lang/String;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem; - public fun equals (Ljava/lang/Object;)Z - public final fun getBlocks ()Ljava/util/List; - public final fun getHasCheckbox ()Ljava/lang/Boolean; - public final fun getLabelType ()Ljava/lang/String; - public final fun getValue ()Ljava/lang/Integer; - public fun hashCode ()I - public final fun isChecked ()Ljava/lang/Boolean; - public fun toString ()Ljava/lang/String; -} - -public final synthetic class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; + public abstract fun getBlocks ()Ljava/util/List; + public abstract fun getHasCheckbox ()Ljava/lang/Boolean; + public abstract fun isChecked ()Ljava/lang/Boolean; } public final class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Ordered : dev/inmo/tgbotapi/types/rich/InputRichBlockListItem { + public static final field Companion Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Ordered$Companion; + public fun (Ljava/util/List;ILdev/inmo/tgbotapi/types/rich/LabelType;Ljava/lang/Boolean;Ljava/lang/Boolean;)V + public synthetic fun (Ljava/util/List;ILdev/inmo/tgbotapi/types/rich/LabelType;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()I + public final fun component3 ()Ldev/inmo/tgbotapi/types/rich/LabelType; + public final fun component4 ()Ljava/lang/Boolean; + public final fun component5 ()Ljava/lang/Boolean; + public final fun copy (Ljava/util/List;ILdev/inmo/tgbotapi/types/rich/LabelType;Ljava/lang/Boolean;Ljava/lang/Boolean;)Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Ordered; + public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Ordered;Ljava/util/List;ILdev/inmo/tgbotapi/types/rich/LabelType;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Ordered; + public fun equals (Ljava/lang/Object;)Z + public fun getBlocks ()Ljava/util/List; + public fun getHasCheckbox ()Ljava/lang/Boolean; + public final fun getLabelType ()Ldev/inmo/tgbotapi/types/rich/LabelType; + public final fun getValue ()I + public fun hashCode ()I + public fun isChecked ()Ljava/lang/Boolean; + public fun toString ()Ljava/lang/String; +} + +public final class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Ordered$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Serializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Serializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V +} + +public final class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Unordered : dev/inmo/tgbotapi/types/rich/InputRichBlockListItem { + public static final field Companion Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Unordered$Companion; + public fun (Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;)V + public synthetic fun (Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/util/List; + public final fun component2 ()Ljava/lang/Boolean; + public final fun component3 ()Ljava/lang/Boolean; + public final fun copy (Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;)Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Unordered; + public static synthetic fun copy$default (Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Unordered;Ljava/util/List;Ljava/lang/Boolean;Ljava/lang/Boolean;ILjava/lang/Object;)Ldev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Unordered; + public fun equals (Ljava/lang/Object;)Z + public fun getBlocks ()Ljava/util/List; + public fun getHasCheckbox ()Ljava/lang/Boolean; + public fun hashCode ()I + public fun isChecked ()Ljava/lang/Boolean; + public fun toString ()Ljava/lang/String; +} + +public final class dev/inmo/tgbotapi/types/rich/InputRichBlockListItem$Unordered$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + public final class dev/inmo/tgbotapi/types/rich/InputRichBlockMap : dev/inmo/tgbotapi/types/rich/InputRichBlock { public static final field Companion Ldev/inmo/tgbotapi/types/rich/InputRichBlockMap$Companion; public static final field TYPE Ljava/lang/String; @@ -35596,6 +35618,13 @@ public final class dev/inmo/tgbotapi/types/rich/InputRichBlockMedia$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class dev/inmo/tgbotapi/types/rich/InputRichBlockOrderedListBuilder { + public fun ()V + public final fun build ()Ljava/util/List; + public final fun item (ILdev/inmo/tgbotapi/types/rich/LabelType;Ljava/lang/Boolean;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;)V + public static synthetic fun item$default (Ldev/inmo/tgbotapi/types/rich/InputRichBlockOrderedListBuilder;ILdev/inmo/tgbotapi/types/rich/LabelType;Ljava/lang/Boolean;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + public final class dev/inmo/tgbotapi/types/rich/InputRichBlockParagraph : dev/inmo/tgbotapi/types/rich/InputRichBlock { public static final field Companion Ldev/inmo/tgbotapi/types/rich/InputRichBlockParagraph$Companion; public static final field TYPE Ljava/lang/String; @@ -35854,6 +35883,14 @@ public final class dev/inmo/tgbotapi/types/rich/InputRichBlockThinking$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public final class dev/inmo/tgbotapi/types/rich/InputRichBlockUnorderedListBuilder { + public fun ()V + public final fun build ()Ljava/util/List; + public final fun item (Ljava/lang/Boolean;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;)V + public final fun item (Ljava/lang/String;)V + public static synthetic fun item$default (Ldev/inmo/tgbotapi/types/rich/InputRichBlockUnorderedListBuilder;Ljava/lang/Boolean;Ljava/lang/Boolean;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V +} + public final class dev/inmo/tgbotapi/types/rich/InputRichBlockVideo : dev/inmo/tgbotapi/types/rich/InputRichBlockMedia { public static final field Companion Ldev/inmo/tgbotapi/types/rich/InputRichBlockVideo$Companion; public static final field TYPE Ljava/lang/String; @@ -35940,12 +35977,24 @@ public final class dev/inmo/tgbotapi/types/rich/InputRichBlocksBuilder { public final fun divider ()V public final fun footer (Ljava/lang/String;)V public final fun footer (Lkotlin/jvm/functions/Function1;)V + public final fun h1 (Ljava/lang/String;)V + public final fun h1 (Lkotlin/jvm/functions/Function1;)V + public final fun h2 (Ljava/lang/String;)V + public final fun h2 (Lkotlin/jvm/functions/Function1;)V + public final fun h3 (Ljava/lang/String;)V + public final fun h3 (Lkotlin/jvm/functions/Function1;)V + public final fun h4 (Ljava/lang/String;)V + public final fun h4 (Lkotlin/jvm/functions/Function1;)V + public final fun h5 (Ljava/lang/String;)V + public final fun h5 (Lkotlin/jvm/functions/Function1;)V + public final fun h6 (Ljava/lang/String;)V + public final fun h6 (Lkotlin/jvm/functions/Function1;)V public final fun heading (ILkotlin/jvm/functions/Function1;)V public final fun heading (Ljava/lang/String;I)V - public final fun list (Lkotlin/jvm/functions/Function1;)V public final fun map (Ldev/inmo/tgbotapi/types/location/StaticLocation;IIILdev/inmo/tgbotapi/types/rich/RichBlockCaption;)V public static synthetic fun map$default (Ldev/inmo/tgbotapi/types/rich/InputRichBlocksBuilder;Ldev/inmo/tgbotapi/types/location/StaticLocation;IIILdev/inmo/tgbotapi/types/rich/RichBlockCaption;ILjava/lang/Object;)V public final fun mathematicalExpression (Ljava/lang/String;)V + public final fun orderedList (Lkotlin/jvm/functions/Function1;)V public final fun paragraph (Ljava/lang/String;)V public final fun paragraph (Lkotlin/jvm/functions/Function1;)V public final fun photo (Ldev/inmo/tgbotapi/types/media/TelegramMediaPhoto;Ldev/inmo/tgbotapi/types/rich/RichBlockCaption;)V @@ -35961,6 +36010,7 @@ public final class dev/inmo/tgbotapi/types/rich/InputRichBlocksBuilder { public final fun thinking (Ljava/lang/String;)V public final fun thinking (Lkotlin/jvm/functions/Function1;)V public final fun unaryPlus (Ldev/inmo/tgbotapi/types/rich/InputRichBlock;)V + public final fun unorderedList (Lkotlin/jvm/functions/Function1;)V public final fun video (Ldev/inmo/tgbotapi/types/media/TelegramMediaVideo;Ldev/inmo/tgbotapi/types/rich/RichBlockCaption;)V public static synthetic fun video$default (Ldev/inmo/tgbotapi/types/rich/InputRichBlocksBuilder;Ldev/inmo/tgbotapi/types/media/TelegramMediaVideo;Ldev/inmo/tgbotapi/types/rich/RichBlockCaption;ILjava/lang/Object;)V public final fun voiceNote (Ldev/inmo/tgbotapi/types/media/TelegramMediaVoiceNote;Ldev/inmo/tgbotapi/types/rich/RichBlockCaption;)V @@ -36046,6 +36096,69 @@ public final class dev/inmo/tgbotapi/types/rich/InputRichMessageMedia$Companion public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class dev/inmo/tgbotapi/types/rich/LabelType { + public static final field Companion Ldev/inmo/tgbotapi/types/rich/LabelType$Companion; + public abstract fun getTypeSymbol ()Ljava/lang/String; +} + +public final class dev/inmo/tgbotapi/types/rich/LabelType$Companion { + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class dev/inmo/tgbotapi/types/rich/LabelType$Decimals : dev/inmo/tgbotapi/types/rich/LabelType { + public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/LabelType$Decimals; + public fun equals (Ljava/lang/Object;)Z + public fun getTypeSymbol ()Ljava/lang/String; + public fun hashCode ()I + public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; +} + +public final class dev/inmo/tgbotapi/types/rich/LabelType$LettersLowercase : dev/inmo/tgbotapi/types/rich/LabelType { + public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/LabelType$LettersLowercase; + public fun equals (Ljava/lang/Object;)Z + public fun getTypeSymbol ()Ljava/lang/String; + public fun hashCode ()I + public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; +} + +public final class dev/inmo/tgbotapi/types/rich/LabelType$LettersUppercase : dev/inmo/tgbotapi/types/rich/LabelType { + public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/LabelType$LettersUppercase; + public fun equals (Ljava/lang/Object;)Z + public fun getTypeSymbol ()Ljava/lang/String; + public fun hashCode ()I + public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; +} + +public final class dev/inmo/tgbotapi/types/rich/LabelType$RomanLowercase : dev/inmo/tgbotapi/types/rich/LabelType { + public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/LabelType$RomanLowercase; + public fun equals (Ljava/lang/Object;)Z + public fun getTypeSymbol ()Ljava/lang/String; + public fun hashCode ()I + public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; +} + +public final class dev/inmo/tgbotapi/types/rich/LabelType$RomanUppercase : dev/inmo/tgbotapi/types/rich/LabelType { + public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/LabelType$RomanUppercase; + public fun equals (Ljava/lang/Object;)Z + public fun getTypeSymbol ()Ljava/lang/String; + public fun hashCode ()I + public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; +} + +public final class dev/inmo/tgbotapi/types/rich/LabelType$Serializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Ldev/inmo/tgbotapi/types/rich/LabelType$Serializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/inmo/tgbotapi/types/rich/LabelType; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/inmo/tgbotapi/types/rich/LabelType;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V +} + public abstract interface class dev/inmo/tgbotapi/types/rich/RichBlock { public static final field Companion Ldev/inmo/tgbotapi/types/rich/RichBlock$Companion; public abstract fun getHtml ()Ljava/lang/String; diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlockParts.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlockParts.kt index 6672e9b775..a2096f499d 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlockParts.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlockParts.kt @@ -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 InputRichBlockListItem */ -@Serializable -data class InputRichBlockListItem( - @SerialName(blocksField) - val blocks: List, - @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 + val hasCheckbox: Boolean? + val isChecked: Boolean? + + @Suppress("SERIALIZER_TYPE_INCOMPATIBLE") + @Serializable(Serializer::class) + data class Ordered( + @SerialName(blocksField) + override val blocks: List, + @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, + @SerialName(hasCheckboxField) + override val hasCheckbox: Boolean? = null, + @SerialName(isCheckedField) + override val isChecked: Boolean? = null + ) : InputRichBlockListItem + + object Serializer : KSerializer { + @Serializable + private data class Surrogate( + @SerialName(blocksField) + val blocks: List, + @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) + } + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocksDsl.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocksDsl.kt index 6e1e623690..88be62ad1c 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocksDsl.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocksDsl.kt @@ -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() +class InputRichBlockOrderedListBuilder { + private val items = mutableListOf() + + 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 = items.toList() +} + +/** + * Builder of [InputRichBlockListItem.Unordered] items used inside [InputRichBlocksBuilder.unorderedList]. + */ +@RichTextDsl +class InputRichBlockUnorderedListBuilder { + private val items = mutableListOf() 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 = items.toList() + fun build(): List = 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)) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/LabelType.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/LabelType.kt new file mode 100644 index 0000000000..cf8177decc --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/rich/LabelType.kt @@ -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 { + 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) + } + } +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocksDslTest.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocksDslTest.kt index a321e39d3d..371201bd14 100644 --- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocksDslTest.kt +++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/InputRichBlocksDslTest.kt @@ -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) + } } diff --git a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/InputRichMessageSerializationTest.kt b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/InputRichMessageSerializationTest.kt index a78e0e72b4..bb36cb2ae4 100644 --- a/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/InputRichMessageSerializationTest.kt +++ b/tgbotapi.core/src/commonTest/kotlin/dev/inmo/tgbotapi/types/rich/InputRichMessageSerializationTest.kt @@ -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 { + json.decodeFromString(InputRichBlockListItem.Serializer, "{\"blocks\":[],\"value\":1}") + } + assertFailsWith { + json.decodeFromString(InputRichBlockListItem.Serializer, "{\"blocks\":[],\"type\":\"1\"}") + } + assertFailsWith { + json.decodeFromString(LabelType.Serializer, "\"?\"") + } + } + @Test fun requiresExactlyOneOfHtmlMarkdownOrBlocks() { assertFailsWith {