diff --git a/tgbotapi.core/api/tgbotapi.core.api b/tgbotapi.core/api/tgbotapi.core.api index 0b00a56a01..5e84e1fb5c 100644 --- a/tgbotapi.core/api/tgbotapi.core.api +++ b/tgbotapi.core/api/tgbotapi.core.api @@ -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 (Ldev/inmo/tgbotapi/types/chat/PreviewChat;Ldev/inmo/tgbotapi/types/MessageThreadId;JILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (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 diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ChatIdentifier.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ChatIdentifier.kt index 06522ce8d0..77438032d4 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ChatIdentifier.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ChatIdentifier.kt @@ -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 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 { 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 { private val internalSerializer = JsonPrimitive.serializer() @@ -253,6 +353,19 @@ object ChatIdentifierSerializer : KSerializer { } } +/** + * Serializes every [ChatIdentifier] subtype while preserving embedded request context. + * + * Numeric identifiers use the following JSON representations: + * - [ChatId]: a JSON number; + * - [ChatIdWithThreadId]: `"/"`; + * - [ChatIdWithChannelDirectMessageThreadId]: `"/cdm/"`; + * - [BusinessChatId]: `"//"`; + * - [EphemeralChatId]: `"/eph//"`. + * + * [Username] uses the complete username string. Unlike [ChatIdentifierSerializer], numeric subtype context survives a + * serialization round trip. + */ @Suppress("unused") @RiskFeature object FullChatIdentifierSerializer : KSerializer { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/MessageGenerationStopped.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/MessageGenerationStopped.kt index 8cf7314940..4afe38602c 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/MessageGenerationStopped.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/MessageGenerationStopped.kt @@ -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 diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/DraftIdAllocator.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/DraftIdAllocator.kt index 5a2f4e5050..6d033d59fe 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/DraftIdAllocator.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/DraftIdAllocator.kt @@ -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() 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")