1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2026-09-13 19:44:18 +00:00

refactor(core): tighten draft and chat contracts

This commit is contained in:
2026-09-07 00:59:22 +06:00
parent 4a9d6b7b56
commit c2f0bcd98b
4 changed files with 130 additions and 9 deletions

View File

@@ -14790,7 +14790,7 @@ public final class dev/inmo/tgbotapi/types/MenuButtonSerializer : kotlinx/serial
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
}
public final class dev/inmo/tgbotapi/types/MessageGenerationStopped {
public final class dev/inmo/tgbotapi/types/MessageGenerationStopped : dev/inmo/tgbotapi/abstracts/WithPreviewChat {
public static final field Companion Ldev/inmo/tgbotapi/types/MessageGenerationStopped$Companion;
public synthetic fun <init> (Ldev/inmo/tgbotapi/types/chat/PreviewChat;Ldev/inmo/tgbotapi/types/MessageThreadId;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ldev/inmo/tgbotapi/types/chat/PreviewChat;Ldev/inmo/tgbotapi/types/MessageThreadId;JLkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -14800,7 +14800,7 @@ public final class dev/inmo/tgbotapi/types/MessageGenerationStopped {
public final fun copy-KX8XwZc (Ldev/inmo/tgbotapi/types/chat/PreviewChat;Ldev/inmo/tgbotapi/types/MessageThreadId;J)Ldev/inmo/tgbotapi/types/MessageGenerationStopped;
public static synthetic fun copy-KX8XwZc$default (Ldev/inmo/tgbotapi/types/MessageGenerationStopped;Ldev/inmo/tgbotapi/types/chat/PreviewChat;Ldev/inmo/tgbotapi/types/MessageThreadId;JILjava/lang/Object;)Ldev/inmo/tgbotapi/types/MessageGenerationStopped;
public fun equals (Ljava/lang/Object;)Z
public final fun getChat ()Ldev/inmo/tgbotapi/types/chat/PreviewChat;
public fun getChat ()Ldev/inmo/tgbotapi/types/chat/PreviewChat;
public final fun getDraftId-12UPeIw ()J
public final fun getMessageThreadId-S3HF-10 ()Ldev/inmo/tgbotapi/types/MessageThreadId;
public fun hashCode ()I

View File

@@ -17,11 +17,19 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.longOrNull
import kotlin.jvm.JvmInline
/** Telegram URI-scheme prefix used for links handled by Telegram applications. */
const val internalTgAppLinksBeginning = "tg://"
/** Base URL used for public Telegram links. */
const val internalLinkBeginning = "https://t.me"
/** Telegram URI prefix used to open a user by numeric identifier. */
const val internalUserLinkBeginning = "${internalTgAppLinksBeginning}user?id="
/** Reserved Telegram username used as the base of managed-bot creation links. */
const val managedBotNewBotUsername = "newbot"
/** Identifies a Telegram chat either by numeric [IdChatIdentifier] or by [Username]. */
@Serializable(ChatIdentifierSerializer::class)
@ClassCastsIncluded
sealed interface ChatIdentifier
@@ -32,35 +40,67 @@ sealed interface ChatIdentifier
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(ChatIdentifierSerializer::class)
sealed interface IdChatIdentifier : ChatIdentifier {
/** Numeric Telegram chat identifier. */
val chatId: RawChatId
/** Message thread carried as default request context, when available. */
val threadId: MessageThreadId?
get() = null
/** Business connection carried as default request context, when available. */
val businessConnectionId: BusinessConnectionId?
get() = null
/** Direct-message thread carried as default request context, when available. */
val directMessageThreadId: DirectMessageThreadId?
get() = null
/** User receiving an ephemeral message, when available. */
val receiverUser: UserId?
get() = null
/** Existing ephemeral message targeted by an operation, when available. */
val ephemeralMessageId: EphemeralMessageId?
get() = null
companion object {
/**
* Creates [ChatIdWithThreadId] when [threadId] is present, [BusinessChatId] when only
* [businessConnectionId] is present, or [ChatId] when both optional values are absent.
* A non-null [threadId] takes precedence over [businessConnectionId].
*/
operator fun invoke(chatId: RawChatId, threadId: MessageThreadId? = null, businessConnectionId: BusinessConnectionId? = null) = threadId ?.let {
ChatIdWithThreadId(chatId, threadId)
} ?: businessConnectionId ?.let {
BusinessChatId(chatId, businessConnectionId)
} ?: ChatId(chatId)
/** Creates an identifier carrying the supplied [threadId]. */
operator fun invoke(chatId: RawChatId, threadId: MessageThreadId) = ChatIdWithThreadId(chatId, threadId)
/** Creates an identifier carrying the supplied [businessConnectionId]. */
operator fun invoke(chatId: RawChatId, businessConnectionId: BusinessConnectionId) = BusinessChatId(chatId, businessConnectionId)
/** Creates an identifier carrying ephemeral delivery context for [receiverUser]. */
operator fun invoke(chatId: RawChatId, receiverUser: UserId, ephemeralMessageId: EphemeralMessageId? = null) = EphemeralChatId(chatId, receiverUser, ephemeralMessageId)
}
}
/**
* Numeric Telegram chat identifier without embedded request context.
*
* @property chatId Raw numeric identifier.
*/
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(ChatIdentifierSerializer::class)
@JvmInline
value class ChatId(override val chatId: RawChatId) : IdChatIdentifier
/**
* Numeric Telegram chat identifier carrying a default message thread.
*
* @property chatIdWithThreadId Raw chat identifier and message-thread identifier pair.
*/
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(ChatIdentifierSerializer::class)
@JvmInline
@@ -70,9 +110,15 @@ value class ChatIdWithThreadId(val chatIdWithThreadId: Pair<RawChatId, MessageTh
override val threadId: MessageThreadId
get() = chatIdWithThreadId.second
/** Creates an identifier from separate [chatId] and [threadId] values. */
constructor(chatId: RawChatId, threadId: MessageThreadId): this(chatId to threadId)
}
/**
* Numeric Telegram chat identifier carrying a default channel direct-message thread.
*
* @property chatIdWithThreadId Raw chat identifier and direct-message-thread identifier pair.
*/
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(ChatIdentifierSerializer::class)
@JvmInline
@@ -82,8 +128,15 @@ value class ChatIdWithChannelDirectMessageThreadId(val chatIdWithThreadId: Pair<
override val directMessageThreadId: DirectMessageThreadId
get() = chatIdWithThreadId.second
/** Creates an identifier from separate [chatId] and [threadId] values. */
constructor(chatId: RawChatId, threadId: DirectMessageThreadId): this(chatId to threadId)
}
/**
* Numeric Telegram chat identifier carrying a default business connection.
*
* @property chatIdWithBusinessConnectionId Raw chat identifier and business-connection identifier pair.
*/
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(ChatIdentifierSerializer::class)
@JvmInline
@@ -93,12 +146,16 @@ value class BusinessChatId(val chatIdWithBusinessConnectionId: Pair<RawChatId, B
override val businessConnectionId: BusinessConnectionId
get() = chatIdWithBusinessConnectionId.second
/** Creates an identifier from separate [chatId] and [businessConnectionId] values. */
constructor(chatId: RawChatId, businessConnectionId: BusinessConnectionId): this(chatId to businessConnectionId)
}
/**
* Chat id of group/supergroup additionally carrying ephemeral [receiverUser] (and optional [ephemeralMessageId]);
* used to default `receiverUserId`/`ephemeralMessageId` params of ephemeral-related requests
*
* @property chatIdWithReceiverUserAndEphemeralMessageId Raw chat identifier, receiver identifier, and optional
* ephemeral-message identifier triple.
*/
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@Serializable(ChatIdentifierSerializer::class)
@@ -113,24 +170,31 @@ value class EphemeralChatId(
override val ephemeralMessageId: EphemeralMessageId?
get() = chatIdWithReceiverUserAndEphemeralMessageId.third
/** Creates an identifier from separate chat, receiver, and optional ephemeral-message identifiers. */
constructor(chatId: RawChatId, receiverUser: UserId, ephemeralMessageId: EphemeralMessageId? = null): this(Triple(chatId, receiverUser, ephemeralMessageId))
}
/** Returns the embedded message thread, or `null` when no message thread is available. */
val ChatIdentifier.threadId: MessageThreadId?
get() = (this as? IdChatIdentifier) ?.threadId
/** Returns the embedded channel direct-message thread, or `null` when no direct-message thread is available. */
val ChatIdentifier.directMessageThreadId: DirectMessageThreadId?
get() = (this as? IdChatIdentifier) ?.directMessageThreadId
/** Returns the embedded business connection, or `null` when no business connection is available. */
val ChatIdentifier.businessConnectionId: BusinessConnectionId?
get() = (this as? IdChatIdentifier) ?.businessConnectionId
/** Returns the embedded ephemeral-message receiver, or `null` when no receiver is available. */
val ChatIdentifier.receiverUser: UserId?
get() = (this as? IdChatIdentifier) ?.receiverUser
/** Returns the embedded ephemeral-message identifier, or `null` when no identifier is available. */
val ChatIdentifier.ephemeralMessageId: EphemeralMessageId?
get() = (this as? IdChatIdentifier) ?.ephemeralMessageId
/** Drops all embedded request context and returns a plain [ChatId]. */
fun IdChatIdentifier.toChatId() = when (this) {
is ChatId -> this
is ChatIdWithThreadId -> ChatId(chatId)
@@ -139,9 +203,16 @@ fun IdChatIdentifier.toChatId() = when (this) {
is EphemeralChatId -> ChatId(chatId)
}
/** Replaces embedded request context with the supplied message [threadId]. */
fun IdChatIdentifier.toChatWithThreadId(threadId: MessageThreadId) = IdChatIdentifier(chatId, threadId)
/** Replaces embedded request context with the supplied channel direct-message [threadId]. */
fun IdChatIdentifier.toChatIdWithChannelDirectMessageThreadId(threadId: DirectMessageThreadId) = ChatIdWithChannelDirectMessageThreadId(chatId, threadId)
/** Replaces embedded request context with the supplied [businessConnectionId]. */
fun IdChatIdentifier.toBusinessChatId(businessConnectionId: BusinessConnectionId) = IdChatIdentifier(chatId, businessConnectionId)
/** Replaces embedded request context with ephemeral delivery context for [receiverUser]. */
fun IdChatIdentifier.toEphemeralChatId(receiverUser: UserId, ephemeralMessageId: EphemeralMessageId? = null) = IdChatIdentifier(chatId, receiverUser, ephemeralMessageId)
/**
@@ -156,14 +227,24 @@ val RawChatId.userLink: String
@Warning("This API have restrictions in Telegram System")
val UserId.userLink: String
get() = chatId.userLink
/** Telegram URI opening the represented [User]. */
val User.userLink: String
get() = id.toChatId().userLink
/** Numeric chat identifier used where Telegram expects a user identifier. */
typealias UserId = IdChatIdentifier
/** Wraps a raw numeric identifier as [ChatId]. */
fun RawChatId.toChatId(): ChatId = ChatId(this)
/** Converts a [Long] value to [ChatId]. */
fun Long.toChatId(): ChatId = ChatId(RawChatId(this))
/** Converts an [Int] value to a numeric chat identifier. */
fun Int.toChatId(): IdChatIdentifier = RawChatId(toLong()).toChatId()
/** Converts a [Byte] value to a numeric chat identifier. */
fun Byte.toChatId(): IdChatIdentifier = RawChatId(toLong()).toChatId()
/**
@@ -209,6 +290,7 @@ value class Username (
}
companion object {
/** Serializes [Username] without the leading `@` and accepts input with or without the prefix. */
object WithoutAtSerializer : KSerializer<Username> {
override val descriptor: SerialDescriptor = String.serializer().descriptor
override fun deserialize(decoder: Decoder): Username = Username.prepare(decoder.decodeString())
@@ -229,8 +311,26 @@ value class Username (
}
}
/** Converts a string with or without a leading `@` to [Username]. */
fun String.toUsername(): Username = Username.prepare(this)
/**
* A custom serializer for the [ChatIdentifier] sealed interface.
*
* This serializer manages the conversion between the [ChatIdentifier] data structure
* and its JSON representation, enabling compatibility with the serialization and deserialization
* processes.
*
* It supports two primary types of [ChatIdentifier]: [IdChatIdentifier] and [Username].
* - For [IdChatIdentifier], it serializes to a numeric ID (e.g., `Long`).
* - For [Username], it serializes to a string prefixed with "@".
*
* Deserialization logic determines whether the input is a numeric ID or a string,
* and converts it to the corresponding subtype of [ChatIdentifier].
*
* Marked with the [RiskFeature] annotation, this class may have certain limitations
* and specific usage considerations tied to the underlying Telegram system.
*/
@RiskFeature
object ChatIdentifierSerializer : KSerializer<ChatIdentifier> {
private val internalSerializer = JsonPrimitive.serializer()
@@ -253,6 +353,19 @@ object ChatIdentifierSerializer : KSerializer<ChatIdentifier> {
}
}
/**
* Serializes every [ChatIdentifier] subtype while preserving embedded request context.
*
* Numeric identifiers use the following JSON representations:
* - [ChatId]: a JSON number;
* - [ChatIdWithThreadId]: `"<chatId>/<threadId>"`;
* - [ChatIdWithChannelDirectMessageThreadId]: `"<chatId>/cdm/<directMessageThreadId>"`;
* - [BusinessChatId]: `"<chatId>//<businessConnectionId>"`;
* - [EphemeralChatId]: `"<chatId>/eph/<receiverUserId>/<ephemeralMessageId>"`.
*
* [Username] uses the complete username string. Unlike [ChatIdentifierSerializer], numeric subtype context survives a
* serialization round trip.
*/
@Suppress("unused")
@RiskFeature
object FullChatIdentifierSerializer : KSerializer<ChatIdentifier> {

View File

@@ -1,5 +1,6 @@
package dev.inmo.tgbotapi.types
import dev.inmo.tgbotapi.abstracts.WithPreviewChat
import dev.inmo.tgbotapi.types.chat.PreviewChat
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -10,9 +11,9 @@ import kotlinx.serialization.Serializable
@Serializable
data class MessageGenerationStopped(
@SerialName(chatField)
val chat: PreviewChat,
override val chat: PreviewChat,
@SerialName(messageThreadIdField)
val messageThreadId: MessageThreadId? = null,
@SerialName(draftIdField)
val draftId: DraftId
)
) : WithPreviewChat

View File

@@ -1,20 +1,27 @@
package dev.inmo.tgbotapi.utils
import dev.inmo.micro_utils.coroutines.suspendPoint
import dev.inmo.tgbotapi.types.DraftId
import kotlinx.coroutines.NonCancellable.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.math.absoluteValue
import kotlin.random.Random
class DraftIdAllocator {
val allocated = mutableSetOf<DraftId>()
val mutex = Mutex()
suspend fun allocate(): DraftId = mutex.withLock {
while (isActive) {
val draftId = DraftId(Random.nextLong())
if (allocated.add(draftId)) {
return draftId
suspend fun allocate(): DraftId {
mutex.withLock {
while (true) {
suspendPoint() // required to be able for current code to stop
val id = Random.nextLong()
if (id == 0L) continue
val draftId = DraftId(id)
if (allocated.add(draftId)) {
return draftId
}
}
}
error("Unable to allocate a unique draft ID")