1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2026-08-19 15:46:31 +00:00

Add Bot API 10.2 subscription updates support

New BotSubscriptionUpdated payload (user, invoicePayload, typed state:
Canceled/Active/Failed/Unknown) in dev.inmo.tgbotapi.types.payments and the
BotSubscriptionUpdatedUpdate for the new subscription update
(UPDATE_SUBSCRIPTION), FlowsUpdatesFilter.botSubscriptionUpdatedUpdatesFlow,
onBotSubscriptionUpdated trigger, waitBotSubscriptionUpdated expectation and
class casts. Notes the Telegram-side Mini App method-origin security hardening.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-18 21:07:42 +06:00
parent 0c329326f7
commit 3d47034f54
15 changed files with 351 additions and 0 deletions

View File

@@ -28,6 +28,8 @@ const val UPDATE_GUEST_MESSAGE = "guest_message"
const val UPDATE_MANAGED_BOT = "managed_bot"
const val UPDATE_SUBSCRIPTION = "subscription"
val ALL_UPDATES_LIST_WITHOUT_REACTIONS = listOf(
UPDATE_MESSAGE,
UPDATE_EDITED_MESSAGE,
@@ -65,6 +67,8 @@ val ALL_UPDATES_LIST_WITHOUT_REACTIONS = listOf(
UPDATE_REMOVE_CHAT_BOOST,
UPDATE_MANAGED_BOT,
UPDATE_SUBSCRIPTION,
)
val ALL_UPDATES_LIST = ALL_UPDATES_LIST_WITHOUT_REACTIONS + listOf(

View File

@@ -0,0 +1,85 @@
@file:OptIn(ExperimentalSerializationApi::class)
package dev.inmo.tgbotapi.types.payments
import dev.inmo.tgbotapi.abstracts.WithUser
import dev.inmo.tgbotapi.types.chat.User
import dev.inmo.tgbotapi.types.invoicePayloadField
import dev.inmo.tgbotapi.types.stateField
import dev.inmo.tgbotapi.types.userField
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
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
import kotlin.jvm.JvmInline
/**
* Information about changes to a user payment subscription toward the current bot
*
* @param user User who subscribed for payments toward the bot
* @param invoicePayload Bot-specified invoice payload
* @param state New state of the subscription
*/
@Serializable
data class BotSubscriptionUpdated(
@SerialName(userField)
override val user: User,
@SerialName(invoicePayloadField)
val invoicePayload: String,
@SerialName(stateField)
val state: State
) : WithUser {
@Serializable(StateSerializer::class)
sealed interface State {
val name: String
@Serializable
data object Canceled : State {
@EncodeDefault
override val name = "canceled"
}
@Serializable
data object Active : State {
@EncodeDefault
override val name = "active"
}
@Serializable
data object Failed : State {
@EncodeDefault
override val name = "failed"
}
@Serializable
@JvmInline
value class Unknown(override val name: String) : State
}
private object StateSerializer : KSerializer<State> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
serialName = "dev.inmo.tgbotapi.types.payments.BotSubscriptionUpdated.State",
kind = PrimitiveKind.STRING
)
override fun deserialize(decoder: Decoder): State {
val value = decoder.decodeString()
return when (value) {
State.Canceled.name -> State.Canceled
State.Active.name -> State.Active
State.Failed.name -> State.Failed
else -> State.Unknown(value)
}
}
override fun serialize(encoder: Encoder, value: State) {
encoder.encodeString(value.name)
}
}
}

View File

@@ -0,0 +1,12 @@
package dev.inmo.tgbotapi.types.update
import dev.inmo.tgbotapi.types.UpdateId
import dev.inmo.tgbotapi.types.payments.BotSubscriptionUpdated
import dev.inmo.tgbotapi.types.update.abstracts.Update
import kotlinx.serialization.Serializable
@Serializable
data class BotSubscriptionUpdatedUpdate(
override val updateId: UpdateId,
override val data: BotSubscriptionUpdated
) : Update

View File

@@ -15,6 +15,7 @@ import dev.inmo.tgbotapi.types.chat.member.ChatMemberUpdated
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.payments.PreCheckoutQuery
import dev.inmo.tgbotapi.types.payments.ShippingQuery
import dev.inmo.tgbotapi.types.polls.Poll
@@ -69,6 +70,7 @@ internal data class RawUpdate constructor(
private val guest_message: RequestGuestContentMessage<*>? = null,
private val purchased_paid_media: PaidMediaPurchased? = null,
private val managed_bot: ManagedBotUpdated? = null,
private val subscription: BotSubscriptionUpdated? = null,
) {
@Transient
private var initedUpdate: Update? = null
@@ -107,6 +109,7 @@ internal data class RawUpdate constructor(
guest_message != null -> GuestMessageUpdate(updateId, guest_message)
purchased_paid_media != null -> PaidMediaPurchasedUpdate(updateId, purchased_paid_media)
managed_bot != null -> ManagedBotUpdate(updateId, managed_bot)
subscription != null -> BotSubscriptionUpdatedUpdate(updateId, subscription)
else -> UnknownUpdate(
updateId,
raw

View File

@@ -47,6 +47,7 @@ interface FlowsUpdatesFilter : UpdatesFilter {
val guestMessageUpdatesFlow: Flow<GuestMessageUpdate>
val paidMediaPurchasedUpdatesFlow: Flow<PaidMediaPurchasedUpdate>
val managedBotUpdatesFlow: Flow<ManagedBotUpdate>
val botSubscriptionUpdatedUpdatesFlow: Flow<BotSubscriptionUpdatedUpdate>
val unknownUpdatesFlow: Flow<UnknownUpdate>
}
@@ -77,6 +78,7 @@ abstract class AbstractFlowsUpdatesFilter : FlowsUpdatesFilter {
override val guestMessageUpdatesFlow: Flow<GuestMessageUpdate> by lazy { allUpdatesFlow.filterIsInstance() }
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() }
}
/**