Add Bot API 10.3 General support

This commit is contained in:
InsanusMokrassar committed 2026-08-25 14:02:46 +06:00
1 parent f86bb6662e
commit 93a4bf1da0
30 files changed
+684 -111

No files matched your search

@@ -25,28 +25,36 @@ fun SendMessageDraft(
draftId: DraftId,
text: String,
parseMode: ParseMode? = null,
threadId: MessageThreadId? = chatId.threadId
threadId: MessageThreadId? = chatId.threadId,
canStop: Boolean? = null,
keepOnStop: Boolean? = null
) = SendMessageDraft(
chatId = chatId,
draftId = draftId,
text = text,
parseMode = parseMode,
rawEntities = null,
threadId = threadId
threadId = threadId,
canStop = canStop,
keepOnStop = keepOnStop
)
fun SendMessageDraft(
chatId: IdChatIdentifier,
draftId: DraftId,
entities: TextSourcesList,
threadId: MessageThreadId? = chatId.threadId
threadId: MessageThreadId? = chatId.threadId,
canStop: Boolean? = null,
keepOnStop: Boolean? = null
) = SendMessageDraft(
chatId = chatId,
draftId = draftId,
text = entities.makeString(),
parseMode = null,
rawEntities = entities.toRawMessageEntities(),
threadId = threadId
threadId = threadId,
canStop = canStop,
keepOnStop = keepOnStop
)
@ConsistentCopyVisibility
@@ -65,7 +73,11 @@ data class SendMessageDraft internal constructor(
@OptIn(ExperimentalSerializationApi::class)
@SerialName(messageThreadIdField)
@EncodeDefault
override val threadId: MessageThreadId? = chatId.threadId
override val threadId: MessageThreadId? = chatId.threadId,
@SerialName(canStopField)
val canStop: Boolean? = null,
@SerialName(keepOnStopField)
val keepOnStop: Boolean? = null
) : SendChatMessageRequest<Unit>,
TextedOutput,
OptionallyMessageThreadRequest
@@ -3,8 +3,10 @@ package dev.inmo.tgbotapi.requests.send
import dev.inmo.tgbotapi.requests.abstracts.SimpleRequest
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.MessageThreadId
import dev.inmo.tgbotapi.types.canStopField
import dev.inmo.tgbotapi.types.chatIdField
import dev.inmo.tgbotapi.types.draftIdField
import dev.inmo.tgbotapi.types.keepOnStopField
import dev.inmo.tgbotapi.types.messageThreadIdField
import dev.inmo.tgbotapi.types.richMessageField
import dev.inmo.tgbotapi.types.rich.InputRichMessage
@@ -34,7 +36,11 @@ data class SendRichMessageDraft(
@SerialName(richMessageField)
val richMessage: InputRichMessage,
@SerialName(messageThreadIdField)
val threadId: MessageThreadId? = chatId.threadId
val threadId: MessageThreadId? = chatId.threadId,
@SerialName(canStopField)
val canStop: Boolean? = null,
@SerialName(keepOnStopField)
val keepOnStop: Boolean? = null
) : SimpleRequest<Unit> {
init {
require(draftId != 0L) {
@@ -630,6 +630,9 @@ const val subscriptionPriceField = "subscription_price"
const val copyTextField = "copy_text"
const val topicIdField = "topic_id"
const val draftIdField = "draft_id"
const val canStopField = "can_stop"
const val keepOnStopField = "keep_on_stop"
const val stoppedMessageGenerationField = "stopped_message_generation"
const val isPublicField = "is_public"
@@ -0,0 +1,18 @@
package dev.inmo.tgbotapi.types
import dev.inmo.tgbotapi.types.chat.PreviewChat
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Information about a user stopping message generation.
*/
@Serializable
data class MessageGenerationStopped(
@SerialName(chatField)
val chat: PreviewChat,
@SerialName(messageThreadIdField)
val messageThreadId: MessageThreadId? = null,
@SerialName(draftIdField)
val draftId: DraftId
)
@@ -30,6 +30,8 @@ const val UPDATE_MANAGED_BOT = "managed_bot"
const val UPDATE_SUBSCRIPTION = "subscription"
const val UPDATE_STOPPED_MESSAGE_GENERATION = stoppedMessageGenerationField
val ALL_UPDATES_LIST_WITHOUT_REACTIONS = listOf(
UPDATE_MESSAGE,
UPDATE_EDITED_MESSAGE,
@@ -69,6 +71,8 @@ val ALL_UPDATES_LIST_WITHOUT_REACTIONS = listOf(
UPDATE_MANAGED_BOT,
UPDATE_SUBSCRIPTION,
UPDATE_STOPPED_MESSAGE_GENERATION,
)
val ALL_UPDATES_LIST = ALL_UPDATES_LIST_WITHOUT_REACTIONS + listOf(
@@ -0,0 +1,15 @@
package dev.inmo.tgbotapi.types.communities
import dev.inmo.tgbotapi.types.communityField
import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.CommonEvent
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Service message about a chat being joined by a user from a community.
*/
@Serializable
data class CommunityChatJoined(
@SerialName(communityField)
val community: Community
) : CommonEvent
@@ -237,7 +237,7 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
* * For all other kinds see [UniqueGift.Common]
*/
@Serializable(UniqueGift.Companion::class)
sealed interface UniqueGift : GiftSentOrReceivedEvent {
sealed interface UniqueGift : GiftSentOrReceivedEvent, TextedInput {
override val gift: Gift.Unique
val origin: String?
val originTyped: Origin?
@@ -246,6 +246,7 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
val lastResaleCurrency: String?
val lastResaleAmount: Long?
val transferStarCount: Int?
val isPrivate: Boolean
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(Origin.Companion::class)
@@ -297,6 +298,12 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
override val gift: Gift.Unique,
@SerialName(originField)
override val originTyped: Origin? = null,
@SerialName(textField)
override val text: String? = null,
@SerialName(entitiesField)
private val entities: RawMessageEntities? = null,
@SerialName(isPrivateField)
override val isPrivate: Boolean = false,
@Deprecated("Use lastResaleCurrency and lastResaleAmount instead")
@SerialName(lastResaleStarCountField)
override val lastResaleStarCount: Int? = null,
@@ -312,12 +319,19 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
override val ownedGiftId: GiftId?
get() = null
override val textSources: List<TextSource> by lazy {
entities ?.asTextSources(text ?: return@lazy emptyList()) ?: emptyList()
}
@Transient
override val origin: String? = originTyped ?.string
constructor(
gift: Gift.Unique,
origin: String?,
text: String? = null,
entities: RawMessageEntities? = null,
isPrivate: Boolean = false,
lastResaleStarCount: Int? = null,
lastResaleCurrency: Currency? = null,
lastResaleAmount: Long? = null,
@@ -326,6 +340,9 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
) : this(
gift,
origin ?.let { Origin.fromString(it) },
text,
entities,
isPrivate,
lastResaleStarCount,
lastResaleCurrency,
lastResaleAmount,
@@ -342,6 +359,12 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
override val ownedGiftId: GiftId,
@SerialName(originField)
override val originTyped: Origin? = null,
@SerialName(textField)
override val text: String? = null,
@SerialName(entitiesField)
private val entities: RawMessageEntities? = null,
@SerialName(isPrivateField)
override val isPrivate: Boolean = false,
@Deprecated("Use lastResaleCurrency and lastResaleAmount instead")
@SerialName(lastResaleStarCountField)
override val lastResaleStarCount: Int? = null,
@@ -357,10 +380,17 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
@Transient
override val origin: String? = originTyped ?.string
override val textSources: List<TextSource> by lazy {
entities ?.asTextSources(text ?: return@lazy emptyList()) ?: emptyList()
}
constructor(
gift: Gift.Unique,
ownedGiftId: GiftId,
origin: String? = null,
text: String? = null,
entities: RawMessageEntities? = null,
isPrivate: Boolean = false,
lastResaleStarCount: Int? = null,
lastResaleCurrency: Currency? = null,
lastResaleAmount: Long? = null,
@@ -370,6 +400,9 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
gift = gift,
ownedGiftId = ownedGiftId,
originTyped = origin ?.let { Origin.fromString(it) },
text = text,
entities = entities,
isPrivate = isPrivate,
lastResaleStarCount = lastResaleStarCount,
lastResaleCurrency = lastResaleCurrency,
lastResaleAmount = lastResaleAmount,
@@ -387,6 +420,12 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
val ownedGiftId: GiftId? = null,
@SerialName(originField)
val origin: Origin? = null,
@SerialName(textField)
val text: String? = null,
@SerialName(entitiesField)
val entities: RawMessageEntities? = null,
@SerialName(isPrivateField)
val isPrivate: Boolean = false,
@SerialName(lastResaleStarCountField)
val lastResaleStarCount: Int? = null,
@SerialName(lastResaleCurrencyField)
@@ -417,6 +456,9 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
Common(
gift = surrogate.gift,
originTyped = surrogate.origin,
text = surrogate.text,
entities = surrogate.entities,
isPrivate = surrogate.isPrivate,
lastResaleStarCount = surrogate.lastResaleStarCount,
lastResaleCurrency = surrogate.lastResaleCurrency,
lastResaleAmount = surrogate.lastResaleAmount,
@@ -429,6 +471,9 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
gift = surrogate.gift,
ownedGiftId = surrogate.ownedGiftId,
originTyped = surrogate.origin,
text = surrogate.text,
entities = surrogate.entities,
isPrivate = surrogate.isPrivate,
lastResaleStarCount = surrogate.lastResaleStarCount,
lastResaleCurrency = surrogate.lastResaleCurrency,
lastResaleAmount = surrogate.lastResaleAmount,
@@ -443,18 +488,28 @@ sealed interface GiftSentOrReceivedEvent : CommonEvent {
gift: Gift.Unique,
origin: String? = null,
ownedGiftId: GiftId? = null,
transferStarCount: Int? = null
transferStarCount: Int? = null,
text: String? = null,
textSources: TextSourcesList = emptyList(),
position: Int = 0,
isPrivate: Boolean = false
) = ownedGiftId ?.let {
ReceivedInBusinessAccount(
gift,
ownedGiftId,
origin,
transferStarCount,
gift = gift,
ownedGiftId = ownedGiftId,
origin = origin,
transferStarCount = transferStarCount,
text = text,
entities = textSources.toRawMessageEntities(position),
isPrivate = isPrivate
)
} ?: Common(
gift,
origin,
transferStarCount,
gift = gift,
origin = origin,
transferStarCount = transferStarCount,
text = text,
entities = textSources.toRawMessageEntities(position),
isPrivate = isPrivate
)
}
}
@@ -11,6 +11,7 @@ import dev.inmo.tgbotapi.types.checklists.ChecklistTaskId
import dev.inmo.tgbotapi.types.checklists.ChecklistTasksAdded
import dev.inmo.tgbotapi.types.checklists.ChecklistTasksDone
import dev.inmo.tgbotapi.types.communities.CommunityChatAdded
import dev.inmo.tgbotapi.types.communities.CommunityChatJoined
import dev.inmo.tgbotapi.types.communities.CommunityChatRemoved
import dev.inmo.tgbotapi.types.dice.Dice
import dev.inmo.tgbotapi.types.files.*
@@ -210,6 +211,7 @@ internal data class RawMessage(
// Communities
private val community_chat_added: CommunityChatAdded? = null,
private val community_chat_joined: CommunityChatJoined? = null,
private val community_chat_removed: CommunityChatRemoved? = null,
// Channel direct messages
@@ -378,6 +380,7 @@ internal data class RawMessage(
checklist_tasks_done != null -> checklist_tasks_done
checklist_tasks_added != null -> checklist_tasks_added
community_chat_added != null -> community_chat_added
community_chat_joined != null -> community_chat_joined
community_chat_removed != null -> community_chat_removed
direct_message_price_changed != null -> direct_message_price_changed
suggested_post_approved != null -> suggested_post_approved
@@ -0,0 +1,12 @@
package dev.inmo.tgbotapi.types.update
import dev.inmo.tgbotapi.types.MessageGenerationStopped
import dev.inmo.tgbotapi.types.UpdateId
import dev.inmo.tgbotapi.types.update.abstracts.Update
import kotlinx.serialization.Serializable
@Serializable
data class MessageGenerationStoppedUpdate(
override val updateId: UpdateId,
override val data: MessageGenerationStopped
) : Update
@@ -16,6 +16,7 @@ import dev.inmo.tgbotapi.types.managed_bots.ManagedBotUpdated
import dev.inmo.tgbotapi.types.message.abstracts.*
import dev.inmo.tgbotapi.types.message.payments.PaidMediaPurchased
import dev.inmo.tgbotapi.types.payments.BotSubscriptionUpdated
import dev.inmo.tgbotapi.types.MessageGenerationStopped
import dev.inmo.tgbotapi.types.payments.PreCheckoutQuery
import dev.inmo.tgbotapi.types.payments.ShippingQuery
import dev.inmo.tgbotapi.types.polls.Poll
@@ -71,6 +72,8 @@ internal data class RawUpdate constructor(
private val purchased_paid_media: PaidMediaPurchased? = null,
private val managed_bot: ManagedBotUpdated? = null,
private val subscription: BotSubscriptionUpdated? = null,
@SerialName(stoppedMessageGenerationField)
private val stopped_message_generation: MessageGenerationStopped? = null,
) {
@Transient
private var initedUpdate: Update? = null
@@ -110,6 +113,7 @@ internal data class RawUpdate constructor(
purchased_paid_media != null -> PaidMediaPurchasedUpdate(updateId, purchased_paid_media)
managed_bot != null -> ManagedBotUpdate(updateId, managed_bot)
subscription != null -> BotSubscriptionUpdatedUpdate(updateId, subscription)
stopped_message_generation != null -> MessageGenerationStoppedUpdate(updateId, stopped_message_generation)
else -> UnknownUpdate(
updateId,
raw
@@ -48,6 +48,7 @@ interface FlowsUpdatesFilter : UpdatesFilter {
val paidMediaPurchasedUpdatesFlow: Flow<PaidMediaPurchasedUpdate>
val managedBotUpdatesFlow: Flow<ManagedBotUpdate>
val botSubscriptionUpdatedUpdatesFlow: Flow<BotSubscriptionUpdatedUpdate>
val messageGenerationStoppedUpdatesFlow: Flow<MessageGenerationStoppedUpdate>
val unknownUpdatesFlow: Flow<UnknownUpdate>
}
@@ -79,6 +80,7 @@ abstract class AbstractFlowsUpdatesFilter : FlowsUpdatesFilter {
override val paidMediaPurchasedUpdatesFlow: Flow<PaidMediaPurchasedUpdate> by lazy { allUpdatesFlow.filterIsInstance() }
override val managedBotUpdatesFlow: Flow<ManagedBotUpdate> by lazy { allUpdatesFlow.filterIsInstance() }
override val botSubscriptionUpdatedUpdatesFlow: Flow<BotSubscriptionUpdatedUpdate> by lazy { allUpdatesFlow.filterIsInstance() }
override val messageGenerationStoppedUpdatesFlow: Flow<MessageGenerationStoppedUpdate> by lazy { allUpdatesFlow.filterIsInstance() }
}
/**
@@ -0,0 +1,74 @@
package dev.inmo.tgbotapi.types
import dev.inmo.tgbotapi.requests.send.SendMessageDraft
import dev.inmo.tgbotapi.requests.send.SendRichMessageDraft
import dev.inmo.tgbotapi.types.gifts.GiftSentOrReceivedEvent
import dev.inmo.tgbotapi.types.communities.CommunityId
import dev.inmo.tgbotapi.types.message.abstracts.ChatEventMessage
import dev.inmo.tgbotapi.types.rich.InputRichMessageHTML
import dev.inmo.tgbotapi.types.update.MessageGenerationStoppedUpdate
import dev.inmo.tgbotapi.types.update.MessageUpdate
import dev.inmo.tgbotapi.types.update.abstracts.UpdateSerializerWithoutSerialization
import dev.inmo.tgbotapi.utils.nonstrictJsonFormat
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
class GeneralBotApi103Test {
private val json = Json { encodeDefaults = true }
@Test
fun `draft requests serialize generation stop controls`() {
val chatId = ChatId(RawChatId(1L))
val messageDraft = SendMessageDraft(chatId, DraftId(2L), "draft", canStop = true, keepOnStop = true)
val richDraft = SendRichMessageDraft(chatId, 2L, InputRichMessageHTML("draft"), canStop = true, keepOnStop = true)
val messageDraftJson = json.encodeToJsonElement(SendMessageDraft.serializer(), messageDraft).jsonObject
val richDraftJson = json.encodeToJsonElement(SendRichMessageDraft.serializer(), richDraft).jsonObject
assertEquals("true", messageDraftJson[canStopField].toString())
assertEquals("true", messageDraftJson[keepOnStopField].toString())
assertEquals("true", richDraftJson[canStopField].toString())
assertEquals("true", richDraftJson[keepOnStopField].toString())
}
@Test
fun `stopped message generation update deserializes`() {
val update = nonstrictJsonFormat.decodeFromString(
UpdateSerializerWithoutSerialization,
"""{"update_id":1,"stopped_message_generation":{"chat":{"id":2,"type":"private","first_name":"User"},"message_thread_id":3,"draft_id":4}}"""
)
val stoppedUpdate = assertIs<MessageGenerationStoppedUpdate>(update)
assertEquals(ChatId(RawChatId(2L)), stoppedUpdate.data.chat.id)
assertEquals(MessageThreadId(3L), stoppedUpdate.data.messageThreadId)
assertEquals(DraftId(4L), stoppedUpdate.data.draftId)
}
@Test
fun `community chat joined message deserializes`() {
val update = nonstrictJsonFormat.decodeFromString(
UpdateSerializerWithoutSerialization,
"""{"update_id":1,"message":{"message_id":2,"date":3,"chat":{"id":4,"type":"private","first_name":"User"},"community_chat_joined":{"community":{"id":5,"name":"Community"}}}}"""
)
val eventMessage = assertIs<ChatEventMessage<*>>(assertIs<MessageUpdate>(update).data)
val joined = assertIs<dev.inmo.tgbotapi.types.communities.CommunityChatJoined>(eventMessage.chatEvent)
assertEquals(CommunityId(5L), joined.community.id)
}
@Test
fun `unique gift info text fields deserialize into text sources`() {
val uniqueGift = nonstrictJsonFormat.decodeFromString(
GiftSentOrReceivedEvent.UniqueGift,
"""{"gift":{"base_name":"Base","name":"unique","number":1,"model":{"name":"Model","sticker":{"file_id":"model-file","file_unique_id":"model-unique","type":"regular","width":1,"height":1},"rarity_per_mille":1},"symbol":{"name":"Symbol","sticker":{"file_id":"symbol-file","file_unique_id":"symbol-unique","type":"regular","width":1,"height":1},"rarity_per_mille":1},"backdrop":{"name":"Backdrop","colors":{"center_color":1,"edge_color":2,"symbol_color":3,"text_color":4},"rarity_per_mille":1}},"origin":"upgrade","text":"bold","entities":[{"type":"bold","offset":0,"length":4}],"is_private":true}"""
)
assertEquals("bold", uniqueGift.text)
assertEquals(true, uniqueGift.isPrivate)
assertEquals(1, uniqueGift.textSources.size)
}
}