diff --git a/.github/workflows/regular-build.yml b/.github/workflows/regular-build.yml new file mode 100644 index 0000000000..ceab9f2734 --- /dev/null +++ b/.github/workflows/regular-build.yml @@ -0,0 +1,16 @@ +name: Build + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build with Gradle + run: ./gradlew build diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a667251bd..74684b3d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,61 @@ # TelegramBotAPI changelog +## 0.32.0 + +**THIS UPDATE CONTAINS BREAKING CHANGES** + +* `Common`: + * `Version`: + * `MicroUtils`: `0.4.16` -> `0.4.23` + * `Klock`: `0.2.3` -> `0.2.4` + * `Ktor`: `1.5.0` -> `1.5.1` +* `Core`: + * **BREAKING CHANGE** Now `MediaGroupMessage` have a generic type related to `MediaGroupContent` + * Methods and types related to `MediaGroupMessage` have been modified according to their meanings + * **Important Change** `FlowsUpdatesFilter` now is an interface. Old class has been renamed to + `DefaultFlowsUpdatesFilter` and factory method `FlowsUpdatesFilter` has been added + * **PASSPORT** Full support of `Telegram Passport API` + * `PassportData` + * All variants of `EncryptedPassportElement` + * All variants of `SecureValue` + * All variants of `PassportElementError` + * New request `SetPassportDataErrors` + * `Credentials`: + * `EncryptedCredentials` + * `DeryptedCredentials` + * `EndDataCredentials` +* `Behaviour Builder`: + * Trigger and expectation extensions for `MessageContent` (`onContentMessage` and `waitContentMessage`) + * `onMediaGroup` has been replaced + * `waitMediaGroup` has been added + * `onVisualMediaGroup` now is just an alternative to `onVisualGallery` + * `command` and `onCommand` expectations has been added for commands `String` variant + * New extensions `BehaviourContext#oneOf`, `BehaviourContext#parallel` and `Deferred#withAction` + * Several renames: + * `waitAudioMediaGroup` -> `waitAudioMediaGroupContent` + * `waitDocumentMediaGroup` -> `waitDocumentMediaGroupContent` + * `waitMediaGroup` -> `waitAnyMediaGroupContent` + * `waitVisualMediaGroup` -> `waitVisualMediaGroupContent` + * New extensions `BehaviourContext#waitPassportMessagesWith` and `BehaviourContext#waitAnyPassportMessages` + * New extensions `BehaviourContext#onPassportMessage` and `BehaviourContext#onPassportMessageWith` +* `Utils`: + * New `ClassCasts` for + * `Message` + * **PASSPORT** `EncryptedPassportElement` + * **PASSPORT** `PassportElementError` + * **PASSPORT** `SecureValue` + * Several tools for decryption have been added: + * `AESDecryptor` is available for `JVM` platform + * Extensions `EncryptedCredentials#decryptWithPKCS8PrivateKey` are available for `JVM` + platform + * Extensions `EndDataCredentials#decryptData` and `FileCredentials#decryptFile` have been added + * Several extensions `createDecryptor` + * Several extensions `doInDecryptionContextWithPKCS8Key` + * New extension `Flow#passportMessages` + * In most of webhook setting up functions/methods now available parameter `mediaGroupsDebounceTimeMillis` +* `API`: + * **PASSPORT** New extensions `TelegramBot#setPassportDataErrors` + ## 0.31.0 **THIS UPDATE CONTAINS BREAKING CHANGES** diff --git a/gradle.properties b/gradle.properties index ff09a58916..710eeb454a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,15 +8,15 @@ kotlin.incremental.js=true kotlin_version=1.4.21 kotlin_coroutines_version=1.4.2 kotlin_serialisation_runtime_version=1.0.1 -klock_version=2.0.3 +klock_version=2.0.4 uuid_version=0.2.3 -ktor_version=1.5.0 +ktor_version=1.5.1 -micro_utils_version=0.4.16 +micro_utils_version=0.4.23 javax_activation_version=1.1.1 library_group=dev.inmo -library_version=0.31.0 +library_version=0.32.0 github_release_plugin_version=2.2.12 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca8c529fce..b9d52d8cb7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip diff --git a/tgbotapi.core/README.md b/tgbotapi.core/README.md index a36ece9cd3..0b8ed3f0d4 100644 --- a/tgbotapi.core/README.md +++ b/tgbotapi.core/README.md @@ -11,9 +11,6 @@ moments are describing by official [Telegram Bot API](https://core.telegram.org/ ## Compatibility This version compatible with [4th of November 2020 update of TelegramBotAPI (version 5.0)](https://core.telegram.org/bots/api#november-4-2020). -There is only one exception of implemented functionality - Telegram Passport API, which was presented in -[August 2018 update of TelegramBotAPI](https://core.telegram.org/bots/api-changelog#august-27-2018) update. It will be implemented -as soon as possible. ## How to implement library? @@ -149,3 +146,18 @@ Here was used `okhttp` realisation of client, but there are several others engin available on ktor.io site for [client](https://ktor.io/clients/http-client/engines.html) and [server](https://ktor.io/quickstart/artifacts.html) engines. +### Passport + +In case you wish to work with `Telegram Passport`, currently there are several useful things, but most part of working +with decryption and handling is available only on JVM. Next snippet contains example of data decryption on JVM platform: + +```kotlin +passportMessage.passportData.doInDecryptionContextWithPKCS8Key(privateKey) { + val passportDataSecureValue = passport ?.data ?: return@doInDecryptionContextWithPKCS8Key + val passportData = (passportMessage.passportData.data.firstOrNull { it is CommonPassport } ?: return@doInDecryptionContextWithPKCS8Key) as CommonPassport + val decrypted = passportDataSecureValue.decrypt( + passportData.data + ) ?.decodeToString() ?: return@doInDecryptionContextWithPKCS8Key + println(decrypted) +} +``` diff --git a/tgbotapi.core/build.gradle b/tgbotapi.core/build.gradle index 9d12635f6c..1894298a2a 100644 --- a/tgbotapi.core/build.gradle +++ b/tgbotapi.core/build.gradle @@ -46,7 +46,10 @@ kotlin { api "com.soywiz.korlibs.klock:klock:$klock_version" api "com.benasher44:uuid:$uuid_version" + api "dev.inmo:micro_utils.crypto:$micro_utils_version" api "dev.inmo:micro_utils.coroutines:$micro_utils_version" + api "dev.inmo:micro_utils.serialization.base64:$micro_utils_version" + api "dev.inmo:micro_utils.serialization.encapsulator:$micro_utils_version" api "io.ktor:ktor-client-core:$ktor_version" } @@ -73,7 +76,6 @@ kotlin { implementation kotlin('test-junit') } } - jsTest { dependencies { implementation kotlin('test-junit') diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/SetPassportDataErrors.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/SetPassportDataErrors.kt new file mode 100644 index 0000000000..80a039fd40 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/SetPassportDataErrors.kt @@ -0,0 +1,22 @@ +package dev.inmo.tgbotapi.requests + +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.requests.abstracts.SimpleRequest +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.passport.PassportElementError +import kotlinx.serialization.* +import kotlinx.serialization.builtins.serializer + +@Serializable +data class SetPassportDataErrors( + @SerialName(userIdField) + val user: UserId, + @SerialName(errorsField) + val errors: List +) : SimpleRequest { + override val resultDeserializer: DeserializationStrategy + get() = Boolean.serializer() + override fun method(): String = "setPassportDataErrors" + override val requestSerializer: SerializationStrategy<*> + get() = serializer() +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/send/media/SendMediaGroup.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/send/media/SendMediaGroup.kt index df4a949fa1..1352d3a9b9 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/send/media/SendMediaGroup.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/send/media/SendMediaGroup.kt @@ -8,6 +8,10 @@ import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.InputMedia.* import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage import dev.inmo.tgbotapi.types.message.abstracts.TelegramBotAPIMessageDeserializeOnlySerializerClass +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent +import dev.inmo.tgbotapi.types.message.content.abstracts.VisualMediaGroupContent +import dev.inmo.tgbotapi.types.message.content.media.AudioContent +import dev.inmo.tgbotapi.types.message.content.media.DocumentContent import dev.inmo.tgbotapi.utils.* import kotlinx.serialization.* import kotlinx.serialization.builtins.ListSerializer @@ -17,13 +21,13 @@ const val rawSendingMediaGroupsWarning = "Media groups contains restrictions rel " types. Currently it is possible to combine photo + video OR audio OR documents" @RiskFeature(rawSendingMediaGroupsWarning) -fun SendMediaGroup( +fun SendMediaGroup( chatId: ChatIdentifier, media: List, disableNotification: Boolean = false, replyToMessageId: MessageIdentifier? = null, allowSendingWithoutReply: Boolean? = null -): Request> { +): Request>> { if (media.size !in mediaCountInMediaGroup) { throwRangeError("Count of members in media group", mediaCountInMediaGroup, media.size) } @@ -47,14 +51,14 @@ fun SendMediaGroup( allowSendingWithoutReply ) - return if (files.isEmpty()) { + return (if (files.isEmpty()) { data } else { MultipartRequestImpl( data, SendMediaGroupFiles(files) ) - } + }) as Request>> } /** @@ -69,7 +73,7 @@ inline fun SendPlaylist( disableNotification: Boolean = false, replyToMessageId: MessageIdentifier? = null, allowSendingWithoutReply: Boolean? = null -) = SendMediaGroup(chatId, media, disableNotification, replyToMessageId, allowSendingWithoutReply) +) = SendMediaGroup(chatId, media, disableNotification, replyToMessageId, allowSendingWithoutReply) /** * Use this method to be sure that you are correctly sending documents media group @@ -83,7 +87,7 @@ inline fun SendDocumentsGroup( disableNotification: Boolean = false, replyToMessageId: MessageIdentifier? = null, allowSendingWithoutReply: Boolean? = null -) = SendMediaGroup(chatId, media, disableNotification, replyToMessageId, allowSendingWithoutReply) +) = SendMediaGroup(chatId, media, disableNotification, replyToMessageId, allowSendingWithoutReply) /** * Use this method to be sure that you are correctly sending visual media group @@ -98,9 +102,9 @@ inline fun SendVisualMediaGroup( disableNotification: Boolean = false, replyToMessageId: MessageIdentifier? = null, allowSendingWithoutReply: Boolean? = null -) = SendMediaGroup(chatId, media, disableNotification, replyToMessageId, allowSendingWithoutReply) +) = SendMediaGroup(chatId, media, disableNotification, replyToMessageId, allowSendingWithoutReply) -private val messagesListSerializer: KSerializer> +private val messagesListSerializer: KSerializer>> = ListSerializer(TelegramBotAPIMessageDeserializeOnlySerializerClass()) @Serializable @@ -114,7 +118,7 @@ data class SendMediaGroupData internal constructor( override val replyToMessageId: MessageIdentifier? = null, @SerialName(allowSendingWithoutReplyField) override val allowSendingWithoutReply: Boolean? = null -) : DataRequest>, SendMessageRequest> { +) : DataRequest>>, SendMessageRequest>> { @SerialName(mediaField) private val convertedMedia: String get() = buildJsonArray { @@ -127,7 +131,7 @@ data class SendMediaGroupData internal constructor( override fun method(): String = "sendMediaGroup" override val requestSerializer: SerializationStrategy<*> get() = serializer() - override val resultDeserializer: DeserializationStrategy> + override val resultDeserializer: DeserializationStrategy>> get() = messagesListSerializer } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt index c66d9d81a2..44c9955298 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt @@ -357,3 +357,39 @@ const val forceField = "force" const val regularPollType = "regular" const val quizPollType = "quiz" + +const val dataField = "data" +const val credentialsField = "credentials" +const val hashField = "hash" +const val translationField = "translation" +const val translationFileField = "translation_file" +const val fileField = "file" +const val filesField = "files" +const val translationFilesField = "translation_files" +const val frontSideField = "front_side" +const val reverseSideField = "reverse_side" +const val selfieField = "selfie" +const val secretField = "secret" + +const val errorsField = "errors" +const val sourceField = "source" +const val fieldNameField = "field_name" +const val dataHashField = "data_hash" +const val fileHashField = "file_hash" +const val fileHashesField = "file_hashes" +const val messageField = "message" +const val unspecifiedField = "unspecified" + +const val secureDataField = "secure_data" +const val nonceField = "nonce" + +const val personalDetailsField = "personal_details" +const val passportField = "passport" +const val internalPassportField = "internal_passport" +const val driverLicenseField = "driver_license" +const val identityCardField = "identity_card" +const val utilityBillField = "utility_bill" +const val bankStatementField = "bank_statement" +const val rentalAgreementField = "rental_agreement" +const val passportRegistrationField = "passport_registration" +const val temporaryRegistrationField = "temporary_registration" diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/files/abstracts/TelegramMediaFile.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/files/abstracts/TelegramMediaFile.kt index f0614a5f48..149391a69c 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/files/abstracts/TelegramMediaFile.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/files/abstracts/TelegramMediaFile.kt @@ -5,6 +5,7 @@ import dev.inmo.tgbotapi.types.FileUniqueId internal const val fileIdField = "file_id" internal const val fileSizeField = "file_size" +internal const val fileDateField = "file_date" internal const val filePathField = "file_path" /** diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/ChannelMediaGroupMessage.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/ChannelMediaGroupMessage.kt index 70d670d41e..3f3688e702 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/ChannelMediaGroupMessage.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/ChannelMediaGroupMessage.kt @@ -9,14 +9,14 @@ import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent -data class ChannelMediaGroupMessage( +data class ChannelMediaGroupMessage( override val messageId: MessageIdentifier, override val chat: Chat, override val date: DateTime, override val mediaGroupId: MediaGroupIdentifier, - override val content: MediaGroupContent, + override val content: T, override val editDate: DateTime?, override val forwardInfo: ForwardInfo?, override val replyTo: Message?, override val replyMarkup: InlineKeyboardMarkup? -) : MediaGroupMessage +) : MediaGroupMessage diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/CommonMediaGroupMessage.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/CommonMediaGroupMessage.kt index 2694c7d187..49d1118e57 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/CommonMediaGroupMessage.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/CommonMediaGroupMessage.kt @@ -7,15 +7,15 @@ import dev.inmo.tgbotapi.types.chat.abstracts.Chat import dev.inmo.tgbotapi.types.message.abstracts.* import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent -data class CommonMediaGroupMessage( +data class CommonMediaGroupMessage( override val messageId: MessageIdentifier, override val user: User, override val chat: Chat, override val date: DateTime, override val mediaGroupId: MediaGroupIdentifier, - override val content: MediaGroupContent, + override val content: T, override val editDate: DateTime?, override val forwardInfo: ForwardInfo?, override val replyTo: Message?, override val replyMarkup: InlineKeyboardMarkup? -) : MediaGroupMessage, FromUserMessage +) : MediaGroupMessage, FromUserMessage diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/PassportMessage.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/PassportMessage.kt new file mode 100644 index 0000000000..55153e159f --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/PassportMessage.kt @@ -0,0 +1,17 @@ +package dev.inmo.tgbotapi.types.message + +import com.soywiz.klock.DateTime +import dev.inmo.tgbotapi.types.MessageIdentifier +import dev.inmo.tgbotapi.types.User +import dev.inmo.tgbotapi.types.chat.abstracts.Chat +import dev.inmo.tgbotapi.types.message.abstracts.FromUserMessage +import dev.inmo.tgbotapi.types.message.abstracts.Message +import dev.inmo.tgbotapi.types.passport.PassportData + +data class PassportMessage( + override val messageId: MessageIdentifier, + override val chat: Chat, + override val user: User, + override val date: DateTime, + val passportData: PassportData +) : Message, FromUserMessage diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessage.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessage.kt index a51e011d8a..b692284b71 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessage.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/RawMessage.kt @@ -18,6 +18,7 @@ import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent import dev.inmo.tgbotapi.types.message.content.media.* import dev.inmo.tgbotapi.types.message.payments.InvoiceContent import dev.inmo.tgbotapi.types.message.payments.SuccessfulPaymentInfo +import dev.inmo.tgbotapi.types.passport.PassportData import dev.inmo.tgbotapi.types.payments.Invoice import dev.inmo.tgbotapi.types.payments.SuccessfulPayment import dev.inmo.tgbotapi.types.polls.Poll @@ -84,7 +85,7 @@ internal data class RawMessage( private val connected_website: String? = null, // passport property - private val passport_data: Unit? = null, + private val passport_data: PassportData? = null, private val proximity_alert_triggered: ProximityAlertTriggered? = null, private val reply_markup: InlineKeyboardMarkup? = null @@ -324,6 +325,14 @@ internal data class RawMessage( ) else -> error("Unknown type of chat: $chat") } + } ?: passport_data ?.let{ + PassportMessage( + messageId, + chat, + from ?: error("For passport must be provided user, but got null"), + date.asDate, + passport_data + ) } ?: error("Was not found supported type of data") } catch (e: Exception) { UnknownMessageType( diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/abstracts/MediaGroupMessage.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/abstracts/MediaGroupMessage.kt index c78ab44f77..307ba6e78c 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/abstracts/MediaGroupMessage.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/abstracts/MediaGroupMessage.kt @@ -3,6 +3,6 @@ package dev.inmo.tgbotapi.types.message.abstracts import dev.inmo.tgbotapi.types.MediaGroupIdentifier import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent -interface MediaGroupMessage : CommonMessage { +interface MediaGroupMessage : CommonMessage { val mediaGroupId: MediaGroupIdentifier } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/abstracts/MediaGroupContent.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/abstracts/MediaGroupContent.kt index a284c0c4b8..7cf2eb6b16 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/abstracts/MediaGroupContent.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/content/abstracts/MediaGroupContent.kt @@ -1,12 +1,18 @@ package dev.inmo.tgbotapi.types.message.content.abstracts import dev.inmo.tgbotapi.CommonAbstracts.CaptionedInput -import dev.inmo.tgbotapi.types.InputMedia.MediaGroupMemberInputMedia +import dev.inmo.tgbotapi.types.InputMedia.* interface MediaGroupContent : MediaContent, CaptionedInput { fun toMediaGroupMemberInputMedia(): MediaGroupMemberInputMedia } -interface VisualMediaGroupContent : MediaGroupContent -interface AudioMediaGroupContent : MediaGroupContent -interface DocumentMediaGroupContent : MediaGroupContent +interface VisualMediaGroupContent : MediaGroupContent { + override fun toMediaGroupMemberInputMedia(): VisualMediaGroupMemberInputMedia +} +interface AudioMediaGroupContent : MediaGroupContent { + override fun toMediaGroupMemberInputMedia(): AudioMediaGroupMemberInputMedia +} +interface DocumentMediaGroupContent : MediaGroupContent { + override fun toMediaGroupMemberInputMedia(): DocumentMediaGroupMemberInputMedia +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/PassportData.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/PassportData.kt new file mode 100644 index 0000000000..9a55da08ab --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/PassportData.kt @@ -0,0 +1,16 @@ +package dev.inmo.tgbotapi.types.passport + +import dev.inmo.tgbotapi.types.credentialsField +import dev.inmo.tgbotapi.types.dataField +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedCredentials +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElement +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PassportData( + @SerialName(dataField) + val data: List, + @SerialName(credentialsField) + val credentials: EncryptedCredentials +) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/PassportElementError.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/PassportElementError.kt new file mode 100644 index 0000000000..74dfc64364 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/PassportElementError.kt @@ -0,0 +1,266 @@ +@file:Suppress("unused", "EXPERIMENTAL_API_USAGE") + +package dev.inmo.tgbotapi.types.passport + +import dev.inmo.micro_utils.crypto.MD5 +import dev.inmo.micro_utils.crypto.md5 +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.* +import dev.inmo.tgbotapi.types.passport.encrypted.type +import dev.inmo.tgbotapi.utils.nonstrictJsonFormat +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* + +val ByteArray.passportFileHash: MD5 + get() = md5() + +@Serializable(PassportElementErrorSerializer::class) +sealed class PassportElementError { + abstract val source: String + abstract val type: String + abstract val message: String +} + +data class UnknownPassportElementError( + val raw: JsonObject +) : PassportElementError() { + override val source: String = raw[sourceField] ?.jsonPrimitive ?.contentOrNull ?: "" + override val type: String = raw[typeField] ?.jsonPrimitive ?.contentOrNull ?: "" + override val message: String = raw[messageField] ?.jsonPrimitive ?.contentOrNull ?: "" +} + +object PassportElementErrorSerializer : KSerializer { + private val jsonObjectSerializer = JsonObject.serializer() + override val descriptor: SerialDescriptor + get() = jsonObjectSerializer.descriptor + override fun deserialize(decoder: Decoder): PassportElementError { + val json = jsonObjectSerializer.deserialize(decoder) + return when (json[sourceField] ?.jsonPrimitive ?.content) { + "dataField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementErrorDataField.serializer(), json) + "frontSideField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementErrorFrontSide.serializer(), json) + "reverseSideField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementErrorReverseSide.serializer(), json) + "selfieField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementErrorSelfie.serializer(), json) + "fileField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementFileError.serializer(), json) + "filesField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementFilesError.serializer(), json) + "translationFileField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementErrorTranslationFile.serializer(), json) + "translationFilesField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementErrorTranslationFiles.serializer(), json) + "unspecifiedField" -> nonstrictJsonFormat.decodeFromJsonElement(PassportElementErrorUnspecified.serializer(), json) + else -> UnknownPassportElementError(json) + } + } + override fun serialize(encoder: Encoder, value: PassportElementError) { + val neverMindAboutThisVariable = when (value) { + is PassportElementErrorFrontSide -> PassportElementErrorFrontSide.serializer().serialize(encoder, value) + is PassportElementErrorReverseSide -> PassportElementErrorReverseSide.serializer().serialize(encoder, value) + is PassportElementErrorSelfie -> PassportElementErrorSelfie.serializer().serialize(encoder, value) + is PassportElementErrorFile -> PassportElementErrorFile.serializer().serialize(encoder, value) + is PassportElementErrorTranslationFile -> PassportElementErrorTranslationFile.serializer().serialize(encoder, value) + is PassportElementErrorUnspecified -> PassportElementErrorUnspecified.serializer().serialize(encoder, value) + is PassportElementErrorDataField -> PassportElementErrorDataField.serializer().serialize(encoder, value) + is PassportElementErrorFiles -> PassportElementErrorFiles.serializer().serialize(encoder, value) + is PassportElementErrorTranslationFiles -> PassportElementErrorTranslationFiles.serializer().serialize(encoder, value) + is UnknownPassportElementError -> jsonObjectSerializer.serialize(encoder, value.raw) + } + } +} + +@Serializable +sealed class PassportSingleElementError : PassportElementError() { + abstract val elementHash: PassportElementHash +} + +@Serializable +sealed class PassportMultipleElementsError : PassportElementError() { + abstract val elementsHashes: List +} + +@Serializable +sealed class PassportElementFileError : PassportSingleElementError() + +@Serializable +sealed class PassportElementFilesError : PassportMultipleElementsError() + +@Serializable +data class PassportElementErrorDataField( + @SerialName(typeField) + override val type: String, + @SerialName(fieldNameField) + val fieldName: String, + @SerialName(dataHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val elementHash: PassportElementHash, + @SerialName(messageField) + override val message: String +) : PassportSingleElementError() { + @SerialName(sourceField) + @Required + override val source: String = dataField +} +fun EncryptedPassportElementWithData.createDataError(field: String, message: String) = PassportElementErrorDataField( + type, + field, + hash, + message +) + +@Serializable +data class PassportElementErrorFrontSide( + @SerialName(typeField) + override val type: String, + @SerialName(fileHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val elementHash: PassportElementHash, + @SerialName(messageField) + override val message: String +) : PassportElementFileError() { + @SerialName(sourceField) + @Required + override val source: String = frontSideField +} +fun EncryptedPassportElementWithFrontSide.createFrontSideError(message: String, unencryptedFileHash: PassportElementHash) = PassportElementErrorFrontSide( + type, + unencryptedFileHash, + message +) + +@Serializable +data class PassportElementErrorReverseSide( + @SerialName(typeField) + override val type: String, + @SerialName(fileHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val elementHash: PassportElementHash, + @SerialName(messageField) + override val message: String +) : PassportElementFileError() { + @SerialName(sourceField) + @Required + override val source: String = reverseSideField +} +fun EncryptedPassportElementWithReverseSide.createReverseSideError(message: String, unencryptedFileHash: PassportElementHash) = PassportElementErrorReverseSide( + type, + unencryptedFileHash, + message +) +@Serializable +data class PassportElementErrorSelfie( + @SerialName(typeField) + override val type: String, + @SerialName(fileHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val elementHash: PassportElementHash, + @SerialName(messageField) + override val message: String +) : PassportElementFileError() { + @SerialName(sourceField) + @Required + override val source: String = selfieField +} +fun EncryptedPassportElementWithSelfie.createSelfieError(message: String, unencryptedFileHash: PassportElementHash) = PassportElementErrorSelfie( + type, + unencryptedFileHash, + message +) + + +@Serializable +data class PassportElementErrorFile( + @SerialName(typeField) + override val type: String, + @SerialName(fileHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val elementHash: PassportElementHash, + @SerialName(messageField) + override val message: String +) : PassportElementFileError() { + @SerialName(sourceField) + @Required + override val source: String = fileField +} +fun EncryptedPassportElementWithFilesCollection.createFileError(message: String, unencryptedFileHash: PassportElementHash) = PassportElementErrorFile( + type, + unencryptedFileHash, + message +) + +@Serializable +data class PassportElementErrorFiles( + @SerialName(typeField) + override val type: String, + @SerialName(fileHashesField) + override val elementsHashes: List<@Serializable(Base64BytesToFromStringSerializer::class) PassportElementHash>, + @SerialName(messageField) + override val message: String +) : PassportElementFilesError() { + @SerialName(sourceField) + @Required + override val source: String = filesField +} +fun EncryptedPassportElementWithFilesCollection.createFilesError(message: String, unencryptedFileHashes: List) = PassportElementErrorFiles( + type, + unencryptedFileHashes, + message +) + + +@Serializable +data class PassportElementErrorTranslationFile( + @SerialName(typeField) + override val type: String, + @SerialName(fileHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val elementHash: PassportElementHash, + @SerialName(messageField) + override val message: String +) : PassportElementFileError() { + @SerialName(sourceField) + @Required + override val source: String = translationFileField +} +fun EncryptedPassportElementTranslatable.createFileError(message: String, unencryptedFileHash: PassportElementHash) = PassportElementErrorTranslationFile( + type, + unencryptedFileHash, + message +) +@Serializable +data class PassportElementErrorTranslationFiles( + @SerialName(typeField) + override val type: String, + @SerialName(fileHashesField) + override val elementsHashes: List<@Serializable(Base64BytesToFromStringSerializer::class) PassportElementHash>, + @SerialName(messageField) + override val message: String +) : PassportElementFilesError() { + @SerialName(sourceField) + @Required + override val source: String = translationFilesField +} +fun EncryptedPassportElementTranslatable.createFilesError(message: String, unencryptedFileHashes: List) = PassportElementErrorTranslationFiles( + type, + unencryptedFileHashes, + message +) + +@Serializable +data class PassportElementErrorUnspecified( + @SerialName(typeField) + override val type: String, + @SerialName(fileHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val elementHash: PassportElementHash, + @SerialName(messageField) + override val message: String +) : PassportElementFileError() { + @SerialName(sourceField) + @Required + override val source: String = unspecifiedField +} +fun EncryptedPassportElement.createUnspecifiedError(message: String, elementHash: PassportElementHash) = PassportElementErrorUnspecified( + type, + elementHash, + message +) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/DecryptedCredentials.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/DecryptedCredentials.kt new file mode 100644 index 0000000000..c33a6bcdbf --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/DecryptedCredentials.kt @@ -0,0 +1,15 @@ +package dev.inmo.tgbotapi.types.passport.credentials + +import dev.inmo.tgbotapi.types.nonceField +import dev.inmo.tgbotapi.types.passport.decrypted.SecureData +import dev.inmo.tgbotapi.types.secureDataField +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DecryptedCredentials( + @SerialName(secureDataField) + val secureData: SecureData, + @SerialName(nonceField) + val nonce: String +) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/EncryptedCredentials.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/EncryptedCredentials.kt new file mode 100644 index 0000000000..4cd8231c3f --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/EncryptedCredentials.kt @@ -0,0 +1,23 @@ +package dev.inmo.tgbotapi.types.passport.credentials + +import dev.inmo.micro_utils.crypto.SourceBytes +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +typealias EncryptedByBotPublicKeyData = SourceBytes +typealias EncryptedData = SourceBytes + +@Serializable +data class EncryptedCredentials( + @SerialName(dataField) + @Serializable(Base64BytesToFromStringSerializer::class) + val data: EncryptedData, + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + val hash: SourceBytes, + @SerialName(secretField) + @Serializable(Base64BytesToFromStringSerializer::class) + val secret: EncryptedByBotPublicKeyData +) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/EndDataCredentials.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/EndDataCredentials.kt new file mode 100644 index 0000000000..7bcc7838b2 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/credentials/EndDataCredentials.kt @@ -0,0 +1,35 @@ +package dev.inmo.tgbotapi.types.passport.credentials + +import dev.inmo.micro_utils.crypto.SourceBytes +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed class EndDataCredentials { + @Serializable(Base64BytesToFromStringSerializer::class) + abstract val hash: SourceBytes + @Serializable(Base64BytesToFromStringSerializer::class) + abstract val secret: SourceBytes +} + +@Serializable +data class DataCredentials( + @SerialName(dataHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: SourceBytes, + @SerialName(secretField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val secret: SourceBytes +) : EndDataCredentials() + +@Serializable +data class FileCredentials( + @SerialName(fileHashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: SourceBytes, + @SerialName(secretField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val secret: SourceBytes +) : EndDataCredentials() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/AddressSecureValue.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/AddressSecureValue.kt new file mode 100644 index 0000000000..d403e11db3 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/AddressSecureValue.kt @@ -0,0 +1,16 @@ +package dev.inmo.tgbotapi.types.passport.decrypted + +import dev.inmo.tgbotapi.types.dataField +import dev.inmo.tgbotapi.types.passport.credentials.DataCredentials +import dev.inmo.tgbotapi.types.passport.credentials.EndDataCredentials +import dev.inmo.tgbotapi.types.passport.decrypted.abstracts.SecureValueWithData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AddressSecureValue( + @SerialName(dataField) + override val data: DataCredentials +) : SecureValueWithData { + override val credentials: List = listOf(data) +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/IdentityWithReverseSideSecureValue.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/IdentityWithReverseSideSecureValue.kt new file mode 100644 index 0000000000..c05a7a7692 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/IdentityWithReverseSideSecureValue.kt @@ -0,0 +1,41 @@ +package dev.inmo.tgbotapi.types.passport.decrypted + +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.passport.credentials.* +import dev.inmo.tgbotapi.types.passport.decrypted.abstracts.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed class IdentityWithReverseSideSecureValue : SecureValueIdentity, SecureValueWithData, SecureValueWithTranslations, SecureValueWithReverseSide { + override val credentials: List + get() = listOfNotNull(data, frontSide, reverseSide, selfie) + translation +} + +@Serializable +data class DriverLicenseSecureValue( + @SerialName(dataField) + override val data: DataCredentials? = null, + @SerialName(frontSideField) + override val frontSide: FileCredentials? = null, + @SerialName(reverseSideField) + override val reverseSide: FileCredentials? = null, + @SerialName(selfieField) + override val selfie: FileCredentials? = null, + @SerialName(translationField) + override val translation: List = emptyList() +) : IdentityWithReverseSideSecureValue() + +@Serializable +data class IdentityCardSecureValue( + @SerialName(dataField) + override val data: DataCredentials? = null, + @SerialName(frontSideField) + override val frontSide: FileCredentials? = null, + @SerialName(reverseSideField) + override val reverseSide: FileCredentials? = null, + @SerialName(selfieField) + override val selfie: FileCredentials? = null, + @SerialName(translationField) + override val translation: List = emptyList() +) : IdentityWithReverseSideSecureValue() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/OtherDocumentsSecureValue.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/OtherDocumentsSecureValue.kt new file mode 100644 index 0000000000..778a773cbf --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/OtherDocumentsSecureValue.kt @@ -0,0 +1,56 @@ +package dev.inmo.tgbotapi.types.passport.decrypted + +import dev.inmo.tgbotapi.types.filesField +import dev.inmo.tgbotapi.types.passport.credentials.EndDataCredentials +import dev.inmo.tgbotapi.types.passport.credentials.FileCredentials +import dev.inmo.tgbotapi.types.passport.decrypted.abstracts.SecureValueWithFiles +import dev.inmo.tgbotapi.types.passport.decrypted.abstracts.SecureValueWithTranslations +import dev.inmo.tgbotapi.types.translationField +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed class OtherDocumentsSecureValue : SecureValueWithTranslations, SecureValueWithFiles { + override val credentials: List + get() = translation + files +} + +@Serializable +data class UtilityBillSecureValue( + @SerialName(translationField) + override val translation: List = emptyList(), + @SerialName(filesField) + override val files: List = emptyList() +) : OtherDocumentsSecureValue() + +@Serializable +data class BankStatementSecureValue( + @SerialName(translationField) + override val translation: List = emptyList(), + @SerialName(filesField) + override val files: List = emptyList() +) : OtherDocumentsSecureValue() + +@Serializable +data class RentalAgreementSecureValue( + @SerialName(translationField) + override val translation: List = emptyList(), + @SerialName(filesField) + override val files: List = emptyList() +) : OtherDocumentsSecureValue() + +@Serializable +data class PassportRegistrationSecureValue( + @SerialName(translationField) + override val translation: List = emptyList(), + @SerialName(filesField) + override val files: List = emptyList() +) : OtherDocumentsSecureValue() + +@Serializable +data class TemporalRegistrationSecureValue( + @SerialName(translationField) + override val translation: List = emptyList(), + @SerialName(filesField) + override val files: List = emptyList() +) : OtherDocumentsSecureValue() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/PassportSecureValue.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/PassportSecureValue.kt new file mode 100644 index 0000000000..effe727275 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/PassportSecureValue.kt @@ -0,0 +1,38 @@ +package dev.inmo.tgbotapi.types.passport.decrypted + +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.passport.credentials.* +import dev.inmo.tgbotapi.types.passport.decrypted.abstracts.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed class PassportSecureValue : SecureValueIdentity, SecureValueWithData, SecureValueWithTranslations { + override val credentials: List + get() = listOfNotNull(data, frontSide, selfie) + translation +} + +@Serializable +data class CommonPassportSecureValue( + @SerialName(dataField) + override val data: DataCredentials? = null, + @SerialName(frontSideField) + override val frontSide: FileCredentials? = null, + @SerialName(selfieField) + override val selfie: FileCredentials? = null, + @SerialName(translationField) + override val translation: List = emptyList() +) : PassportSecureValue() + +@Serializable +data class InternalPassportSecureValue( + @SerialName(dataField) + override val data: DataCredentials? = null, + @SerialName(frontSideField) + override val frontSide: FileCredentials? = null, + @SerialName(selfieField) + override val selfie: FileCredentials? = null, + @SerialName(translationField) + override val translation: List = emptyList() +) : PassportSecureValue() + diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/PersonalDetailsSecureValue.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/PersonalDetailsSecureValue.kt new file mode 100644 index 0000000000..120b2bf77a --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/PersonalDetailsSecureValue.kt @@ -0,0 +1,16 @@ +package dev.inmo.tgbotapi.types.passport.decrypted + +import dev.inmo.tgbotapi.types.dataField +import dev.inmo.tgbotapi.types.passport.credentials.DataCredentials +import dev.inmo.tgbotapi.types.passport.credentials.EndDataCredentials +import dev.inmo.tgbotapi.types.passport.decrypted.abstracts.SecureValueWithData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PersonalDetailsSecureValue( + @SerialName(dataField) + override val data: DataCredentials +) : SecureValueWithData { + override val credentials: List = listOf(data) +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/SecureData.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/SecureData.kt new file mode 100644 index 0000000000..c2b9092603 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/SecureData.kt @@ -0,0 +1,42 @@ +package dev.inmo.tgbotapi.types.passport.decrypted + +import dev.inmo.tgbotapi.types.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SecureData( + @SerialName(personalDetailsField) + val personalDetails: PersonalDetailsSecureValue? = null, + @SerialName(passportField) + val passport: CommonPassportSecureValue? = null, + @SerialName(internalPassportField) + val internalPassport: InternalPassportSecureValue? = null, + @SerialName(driverLicenseField) + val driverLicense: DriverLicenseSecureValue? = null, + @SerialName(identityCardField) + val identityCard: IdentityCardSecureValue? = null, + @SerialName(utilityBillField) + val utilityBill: UtilityBillSecureValue? = null, + @SerialName(bankStatementField) + val bankStatement: BankStatementSecureValue? = null, + @SerialName(rentalAgreementField) + val rentalAgreement: RentalAgreementSecureValue? = null, + @SerialName(passportRegistrationField) + val passportRegistration: PassportRegistrationSecureValue? = null, + @SerialName(temporaryRegistrationField) + val temporaryRegistration: TemporalRegistrationSecureValue? = null, +) { + val allCredentials by lazy { + (personalDetails ?.credentials ?: emptyList()) + + (passport ?.credentials ?: emptyList()) + + (internalPassport ?.credentials ?: emptyList()) + + (driverLicense ?.credentials ?: emptyList()) + + (identityCard ?.credentials ?: emptyList()) + + (utilityBill ?.credentials ?: emptyList()) + + (bankStatement ?.credentials ?: emptyList()) + + (rentalAgreement ?.credentials ?: emptyList()) + + (passportRegistration ?.credentials ?: emptyList()) + + (temporaryRegistration ?.credentials ?: emptyList()) + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValue.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValue.kt new file mode 100644 index 0000000000..b9bb54371c --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValue.kt @@ -0,0 +1,7 @@ +package dev.inmo.tgbotapi.types.passport.decrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.credentials.EndDataCredentials + +interface SecureValue { + val credentials: List +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueIdentity.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueIdentity.kt new file mode 100644 index 0000000000..e1de83733a --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueIdentity.kt @@ -0,0 +1,8 @@ +package dev.inmo.tgbotapi.types.passport.decrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.credentials.FileCredentials + +interface SecureValueIdentity : SecureValue { + val frontSide: FileCredentials? + val selfie: FileCredentials? +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithData.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithData.kt new file mode 100644 index 0000000000..9e984cd3b0 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithData.kt @@ -0,0 +1,7 @@ +package dev.inmo.tgbotapi.types.passport.decrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.credentials.DataCredentials + +interface SecureValueWithData : SecureValue { + val data: DataCredentials? +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithFiles.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithFiles.kt new file mode 100644 index 0000000000..ea201f1ee8 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithFiles.kt @@ -0,0 +1,7 @@ +package dev.inmo.tgbotapi.types.passport.decrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.credentials.FileCredentials + +interface SecureValueWithFiles : SecureValue { + val files: List +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithReverseSide.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithReverseSide.kt new file mode 100644 index 0000000000..9706c5d041 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithReverseSide.kt @@ -0,0 +1,7 @@ +package dev.inmo.tgbotapi.types.passport.decrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.credentials.FileCredentials + +interface SecureValueWithReverseSide : SecureValue { + val reverseSide: FileCredentials? +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithTranslations.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithTranslations.kt new file mode 100644 index 0000000000..f540a2ad7f --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/decrypted/abstracts/SecureValueWithTranslations.kt @@ -0,0 +1,7 @@ +package dev.inmo.tgbotapi.types.passport.decrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.credentials.FileCredentials + +interface SecureValueWithTranslations : SecureValue { + val translation: List +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/Email.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/Email.kt new file mode 100644 index 0000000000..a8ed36220e --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/Email.kt @@ -0,0 +1,18 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.emailField +import dev.inmo.tgbotapi.types.hashField +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.PassportElementHash +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElementWithEmail +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Email( + @SerialName(emailField) + override val email: String, + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithEmail diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedAddress.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedAddress.kt new file mode 100644 index 0000000000..3e3f1d2010 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedAddress.kt @@ -0,0 +1,18 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.dataField +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedData +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.PassportElementHash +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElementWithData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EncryptedAddress( + @SerialName(dataField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val data: EncryptedData, + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithData diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedElementSerializer.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedElementSerializer.kt new file mode 100644 index 0000000000..52cb81f1af --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedElementSerializer.kt @@ -0,0 +1,64 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.micro_utils.crypto.decodeBase64 +import dev.inmo.micro_utils.serialization.encapsulator.Encapsulator +import dev.inmo.tgbotapi.types.hashField +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElement +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.UnknownEncryptedPassportElement +import dev.inmo.tgbotapi.types.typeField +import dev.inmo.tgbotapi.utils.RiskFeature +import dev.inmo.tgbotapi.utils.nonstrictJsonFormat +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* + +val encryptedElementsClassesByTypes = mapOf( + "personal_details" to Encapsulator(EncryptedPersonalDetails::class, EncryptedPersonalDetails.serializer()), + "passport" to Encapsulator(CommonPassport::class, CommonPassport.serializer()), + "driver_license" to Encapsulator(DriverLicense::class, DriverLicense.serializer()), + "identity_card" to Encapsulator(IdentityCard::class, IdentityCard.serializer()), + "internal_passport" to Encapsulator(InternalPassport::class, InternalPassport.serializer()), + "address" to Encapsulator(EncryptedAddress::class, EncryptedAddress.serializer()), + "utility_bill" to Encapsulator(UtilityBill::class, UtilityBill.serializer()), + "bank_statement" to Encapsulator(BankStatement::class, BankStatement.serializer()), + "rental_agreement" to Encapsulator(RentalAgreement::class, RentalAgreement.serializer()), + "passport_registration" to Encapsulator(PassportRegistration::class, PassportRegistration.serializer()), + "temporary_registration" to Encapsulator(TemporaryRegistration::class, TemporaryRegistration.serializer()), + "phone_number" to Encapsulator(PhoneNumber::class, PhoneNumber.serializer()), + "email" to Encapsulator(Email::class, Email.serializer()) +) + +@RiskFeature("Remember that this method may return \"unknown\" in case if encrypted element was not defined in library") +val EncryptedPassportElement.type: String + get() = encryptedElementsClassesByTypes.keys.firstOrNull { encryptedElementsClassesByTypes.getValue(it).klass.isInstance(this) } ?: "unknown" + +@Serializer(EncryptedPassportElement::class) +object EncryptedElementSerializer : KSerializer { + private val jsonSerializer = JsonObject.serializer() + override val descriptor: SerialDescriptor = jsonSerializer.descriptor + + override fun deserialize(decoder: Decoder): EncryptedPassportElement { + val json = jsonSerializer.deserialize(decoder) + return json[typeField] ?.jsonPrimitive ?.content ?.let { type -> + encryptedElementsClassesByTypes[type] ?.serializer ?.let { deserializer -> + nonstrictJsonFormat.decodeFromJsonElement(deserializer, json) + } + } ?: UnknownEncryptedPassportElement(json, json[hashField] ?.jsonPrimitive ?.content ?.decodeBase64() ?: byteArrayOf()) + } + + override fun serialize(encoder: Encoder, value: EncryptedPassportElement) { + val json = value.let { + encryptedElementsClassesByTypes.forEach { (key, encapsulator) -> + val json = encapsulator.encapsulate(value) { data -> + nonstrictJsonFormat.encodeToJsonElement(this as KSerializer, data).jsonObject + } ?: return@forEach + return@let JsonObject(json + (typeField to JsonPrimitive(key))) + } + (value as? UnknownEncryptedPassportElement) ?.rawJson ?: return + } + jsonSerializer.serialize(encoder, json) + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPassportElementWithTranslatableFilesCollection.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPassportElementWithTranslatableFilesCollection.kt new file mode 100644 index 0000000000..00ecd47924 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPassportElementWithTranslatableFilesCollection.kt @@ -0,0 +1,62 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +sealed class EncryptedPassportElementWithTranslatableFilesCollection : EncryptedPassportElementTranslatable, EncryptedPassportElementWithFilesCollection + +@Serializable +data class UtilityBill( + @SerialName(filesField) + override val files: List, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithTranslatableFilesCollection() +@Serializable +data class BankStatement( + @SerialName(filesField) + override val files: List, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithTranslatableFilesCollection() +@Serializable +data class RentalAgreement( + @SerialName(filesField) + override val files: List, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithTranslatableFilesCollection() +@Serializable +data class PassportRegistration( + @SerialName(filesField) + override val files: List, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithTranslatableFilesCollection() +@Serializable +data class TemporaryRegistration( + @SerialName(filesField) + override val files: List, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithTranslatableFilesCollection() + diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPassportElementWithTranslatableIDDocument.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPassportElementWithTranslatableIDDocument.kt new file mode 100644 index 0000000000..02d587e8bc --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPassportElementWithTranslatableIDDocument.kt @@ -0,0 +1,47 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedData +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +sealed class EncryptedPassportElementWithTranslatableIDDocument : EncryptedPassportElementWithData, EncryptedPassportElementWithFrontSide, EncryptedPassportElementWithReverseSide, EncryptedPassportElementWithSelfie, EncryptedPassportElementTranslatable + +@Serializable +data class DriverLicense( + @SerialName(dataField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val data: EncryptedData, + @SerialName(frontSideField) + override val frontSide: PassportFile? = null, + @SerialName(reverseSideField) + override val reverseSide: PassportFile? = null, + @SerialName(selfieField) + override val selfie: PassportFile? = null, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithTranslatableIDDocument() + +@Serializable +data class IdentityCard( + @SerialName(dataField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val data: EncryptedData, + @SerialName(frontSideField) + override val frontSide: PassportFile? = null, + @SerialName(reverseSideField) + override val reverseSide: PassportFile? = null, + @SerialName(selfieField) + override val selfie: PassportFile? = null, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithTranslatableIDDocument() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPersonalDetails.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPersonalDetails.kt new file mode 100644 index 0000000000..fe1142a7e4 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/EncryptedPersonalDetails.kt @@ -0,0 +1,18 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.dataField +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedData +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.PassportElementHash +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElementWithData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EncryptedPersonalDetails( + @SerialName(dataField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val data: EncryptedData, + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithData diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/Passport.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/Passport.kt new file mode 100644 index 0000000000..e79bcb017c --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/Passport.kt @@ -0,0 +1,42 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedData +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +sealed class Passport : EncryptedPassportElementWithData, EncryptedPassportElementWithFrontSide, EncryptedPassportElementWithSelfie, EncryptedPassportElementTranslatable + +@Serializable +data class CommonPassport( + @SerialName(dataField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val data: EncryptedData, + @SerialName(frontSideField) + override val frontSide: PassportFile? = null, + @SerialName(selfieField) + override val selfie: PassportFile? = null, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : Passport() +@Serializable +data class InternalPassport( + @SerialName(dataField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val data: EncryptedData, + @SerialName(frontSideField) + override val frontSide: PassportFile? = null, + @SerialName(selfieField) + override val selfie: PassportFile? = null, + @SerialName(translationField) + override val translations: List = emptyList(), + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : Passport() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/PassportFile.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/PassportFile.kt new file mode 100644 index 0000000000..675105120b --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/PassportFile.kt @@ -0,0 +1,23 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.tgbotapi.requests.abstracts.FileId +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.files.abstracts.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * This object represents a file uploaded to Telegram Passport. Currently all Telegram Passport files are in JPEG format + * when decrypted and don't exceed 10MB. + */ +@Serializable +data class PassportFile( + @SerialName(fileIdField) + override val fileId: FileId, + @SerialName(fileUniqueIdField) + override val fileUniqueId: FileUniqueId, + @SerialName(fileDateField) + val uploadingDate: TelegramDate, + @SerialName(fileSizeField) + override val fileSize: Long? = null +) : TelegramMediaFile diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/PhoneNumber.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/PhoneNumber.kt new file mode 100644 index 0000000000..88dc723765 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/PhoneNumber.kt @@ -0,0 +1,18 @@ +package dev.inmo.tgbotapi.types.passport.encrypted + +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.hashField +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.PassportElementHash +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElementWithPhoneNumber +import dev.inmo.tgbotapi.types.phoneNumberField +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PhoneNumber( + @SerialName(phoneNumberField) + override val phoneNumber: String, + @SerialName(hashField) + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElementWithPhoneNumber diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElement.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElement.kt new file mode 100644 index 0000000000..6bf1d843df --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElement.kt @@ -0,0 +1,21 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.micro_utils.crypto.SourceBytes +import dev.inmo.micro_utils.serialization.base64.Base64BytesToFromStringSerializer +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject + +typealias PassportElementHash = SourceBytes + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElement { + val hash: PassportElementHash +} + +@Serializable(EncryptedElementSerializer::class) +data class UnknownEncryptedPassportElement( + val rawJson: JsonObject, + @Serializable(Base64BytesToFromStringSerializer::class) + override val hash: PassportElementHash +) : EncryptedPassportElement diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementTranslatable.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementTranslatable.kt new file mode 100644 index 0000000000..32c3a2af89 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementTranslatable.kt @@ -0,0 +1,10 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import dev.inmo.tgbotapi.types.passport.encrypted.PassportFile +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElementTranslatable : EncryptedPassportElement { + val translations: List +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithData.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithData.kt new file mode 100644 index 0000000000..9332b22b0d --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithData.kt @@ -0,0 +1,10 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedData +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElementWithData : EncryptedPassportElement { + val data: EncryptedData +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithEmail.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithEmail.kt new file mode 100644 index 0000000000..e0ddcca903 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithEmail.kt @@ -0,0 +1,9 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElementWithEmail : EncryptedPassportElement { + val email: String +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithFilesCollection.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithFilesCollection.kt new file mode 100644 index 0000000000..e9edc842e5 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithFilesCollection.kt @@ -0,0 +1,10 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import dev.inmo.tgbotapi.types.passport.encrypted.PassportFile +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElementWithFilesCollection : EncryptedPassportElement { + val files: List +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithFrontSide.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithFrontSide.kt new file mode 100644 index 0000000000..dbce257cd9 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithFrontSide.kt @@ -0,0 +1,10 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import dev.inmo.tgbotapi.types.passport.encrypted.PassportFile +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElementWithFrontSide : EncryptedPassportElement { + val frontSide: PassportFile? +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithPhoneNumber.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithPhoneNumber.kt new file mode 100644 index 0000000000..59e4939925 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithPhoneNumber.kt @@ -0,0 +1,9 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElementWithPhoneNumber : EncryptedPassportElement { + val phoneNumber: String +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithReverseSide.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithReverseSide.kt new file mode 100644 index 0000000000..24bf4f1874 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithReverseSide.kt @@ -0,0 +1,10 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import dev.inmo.tgbotapi.types.passport.encrypted.PassportFile +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElementWithReverseSide : EncryptedPassportElement { + val reverseSide: PassportFile? +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithSelfie.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithSelfie.kt new file mode 100644 index 0000000000..d36de7e9cc --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/passport/encrypted/abstracts/EncryptedPassportElementWithSelfie.kt @@ -0,0 +1,10 @@ +package dev.inmo.tgbotapi.types.passport.encrypted.abstracts + +import dev.inmo.tgbotapi.types.passport.encrypted.EncryptedElementSerializer +import dev.inmo.tgbotapi.types.passport.encrypted.PassportFile +import kotlinx.serialization.Serializable + +@Serializable(EncryptedElementSerializer::class) +interface EncryptedPassportElementWithSelfie : EncryptedPassportElement { + val selfie: PassportFile? +} \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/ChannelPostMediaGroupUpdate.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/ChannelPostMediaGroupUpdate.kt index fa4532d80d..505caf47ef 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/ChannelPostMediaGroupUpdate.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/ChannelPostMediaGroupUpdate.kt @@ -2,11 +2,12 @@ package dev.inmo.tgbotapi.types.update.MediaGroupUpdates import dev.inmo.tgbotapi.types.UpdateIdentifier import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent import dev.inmo.tgbotapi.types.update.abstracts.BaseMessageUpdate data class ChannelPostMediaGroupUpdate( override val origins: List ) : SentMediaGroupUpdate { override val updateId: UpdateIdentifier = origins.last().updateId - override val data: List = origins.mapNotNull { it.data as? MediaGroupMessage } + override val data: List> = origins.mapNotNull { it.data as? MediaGroupMessage } } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/EditChannelPostMediaGroupUpdate.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/EditChannelPostMediaGroupUpdate.kt index 596088bbcb..8c0935d199 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/EditChannelPostMediaGroupUpdate.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/EditChannelPostMediaGroupUpdate.kt @@ -2,11 +2,12 @@ package dev.inmo.tgbotapi.types.update.MediaGroupUpdates import dev.inmo.tgbotapi.types.UpdateIdentifier import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent import dev.inmo.tgbotapi.types.update.EditChannelPostUpdate data class EditChannelPostMediaGroupUpdate( override val origin: EditChannelPostUpdate ) : EditMediaGroupUpdate { override val updateId: UpdateIdentifier = origin.updateId - override val data: MediaGroupMessage = origin.data as MediaGroupMessage + override val data: MediaGroupMessage = origin.data as MediaGroupMessage } \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/EditMessageMediaGroupUpdate.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/EditMessageMediaGroupUpdate.kt index 0a88e5c117..075b3c38d2 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/EditMessageMediaGroupUpdate.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/EditMessageMediaGroupUpdate.kt @@ -2,11 +2,12 @@ package dev.inmo.tgbotapi.types.update.MediaGroupUpdates import dev.inmo.tgbotapi.types.UpdateIdentifier import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent import dev.inmo.tgbotapi.types.update.EditMessageUpdate data class EditMessageMediaGroupUpdate( override val origin: EditMessageUpdate ) : EditMediaGroupUpdate { override val updateId: UpdateIdentifier = origin.updateId - override val data: MediaGroupMessage = origin.data as MediaGroupMessage + override val data: MediaGroupMessage = origin.data as MediaGroupMessage } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/MediaGroupUpdate.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/MediaGroupUpdate.kt index 77c7f41f8d..b50e772ba7 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/MediaGroupUpdate.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/MediaGroupUpdate.kt @@ -1,6 +1,7 @@ package dev.inmo.tgbotapi.types.update.MediaGroupUpdates import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent import dev.inmo.tgbotapi.types.update.abstracts.* /** @@ -13,11 +14,11 @@ import dev.inmo.tgbotapi.types.update.abstracts.* interface MediaGroupUpdate : Update interface SentMediaGroupUpdate: MediaGroupUpdate { - override val data: List + override val data: List> val origins: List } interface EditMediaGroupUpdate : BaseEditMessageUpdate, MediaGroupUpdate { - override val data: MediaGroupMessage + override val data: MediaGroupMessage val origin: BaseMessageUpdate } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/MessageMediaGroupUpdate.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/MessageMediaGroupUpdate.kt index f5bb8e69e7..3dd3c86cb0 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/MessageMediaGroupUpdate.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/update/MediaGroupUpdates/MessageMediaGroupUpdate.kt @@ -2,11 +2,12 @@ package dev.inmo.tgbotapi.types.update.MediaGroupUpdates import dev.inmo.tgbotapi.types.UpdateIdentifier import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent import dev.inmo.tgbotapi.types.update.abstracts.BaseMessageUpdate data class MessageMediaGroupUpdate( override val origins: List ) : SentMediaGroupUpdate { override val updateId: UpdateIdentifier = origins.last().updateId - override val data: List = origins.mapNotNull { it.data as? MediaGroupMessage } + override val data: List> = origins.mapNotNull { it.data as? MediaGroupMessage } } \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/updateshandlers/FlowsUpdatesFilter.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/updateshandlers/FlowsUpdatesFilter.kt index e44d863dd5..b07bc97cf6 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/updateshandlers/FlowsUpdatesFilter.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/updateshandlers/FlowsUpdatesFilter.kt @@ -7,15 +7,47 @@ import dev.inmo.tgbotapi.types.update.abstracts.UnknownUpdate import dev.inmo.tgbotapi.types.update.abstracts.Update import kotlinx.coroutines.flow.* -@Suppress("EXPERIMENTAL_API_USAGE", "unused") -class FlowsUpdatesFilter( +interface FlowsUpdatesFilter : UpdatesFilter { + override val allowedUpdates: List + get() = ALL_UPDATES_LIST + val allUpdatesFlow: Flow + val allUpdatesWithoutMediaGroupsGroupingFlow: Flow + + val messageFlow: Flow + val messageMediaGroupFlow: Flow + val editedMessageFlow: Flow + val editedMessageMediaGroupFlow: Flow + val channelPostFlow: Flow + val channelPostMediaGroupFlow: Flow + val editedChannelPostFlow: Flow + val editedChannelPostMediaGroupFlow: Flow + val chosenInlineResultFlow: Flow + val inlineQueryFlow: Flow + val callbackQueryFlow: Flow + val shippingQueryFlow: Flow + val preCheckoutQueryFlow: Flow + val pollFlow: Flow + val pollAnswerFlow: Flow + val unknownUpdateTypeFlow: Flow +} + +/** + * Creates [DefaultFlowsUpdatesFilter] + */ +@Suppress("FunctionName") +fun FlowsUpdatesFilter( broadcastChannelsSize: Int = 100 -): UpdatesFilter { +) = DefaultFlowsUpdatesFilter(broadcastChannelsSize) + +@Suppress("EXPERIMENTAL_API_USAGE", "unused") +class DefaultFlowsUpdatesFilter( + broadcastChannelsSize: Int = 100 +): FlowsUpdatesFilter { private val updatesSharedFlow = MutableSharedFlow(extraBufferCapacity = broadcastChannelsSize) @Suppress("MemberVisibilityCanBePrivate") - val allUpdatesFlow: Flow = updatesSharedFlow.asSharedFlow() + override val allUpdatesFlow: Flow = updatesSharedFlow.asSharedFlow() @Suppress("MemberVisibilityCanBePrivate") - val allUpdatesWithoutMediaGroupsGroupingFlow: Flow = updatesSharedFlow.flatMapConcat { + override val allUpdatesWithoutMediaGroupsGroupingFlow: Flow = allUpdatesFlow.flatMapConcat { when (it) { is SentMediaGroupUpdate -> it.origins.asFlow() is EditMediaGroupUpdate -> flowOf(it.origin) @@ -23,26 +55,24 @@ class FlowsUpdatesFilter( } } - override val allowedUpdates: List - get() = ALL_UPDATES_LIST override val asUpdateReceiver: UpdateReceiver = { updatesSharedFlow.emit(it) } - val messageFlow: Flow = allUpdatesFlow.filterIsInstance() - val messageMediaGroupFlow: Flow = allUpdatesFlow.filterIsInstance() - val editedMessageFlow: Flow = allUpdatesFlow.filterIsInstance() - val editedMessageMediaGroupFlow: Flow = allUpdatesFlow.filterIsInstance() - val channelPostFlow: Flow = allUpdatesFlow.filterIsInstance() - val channelPostMediaGroupFlow: Flow = allUpdatesFlow.filterIsInstance() - val editedChannelPostFlow: Flow = allUpdatesFlow.filterIsInstance() - val editedChannelPostMediaGroupFlow: Flow = allUpdatesFlow.filterIsInstance() - val chosenInlineResultFlow: Flow = allUpdatesFlow.filterIsInstance() - val inlineQueryFlow: Flow = allUpdatesFlow.filterIsInstance() - val callbackQueryFlow: Flow = allUpdatesFlow.filterIsInstance() - val shippingQueryFlow: Flow = allUpdatesFlow.filterIsInstance() - val preCheckoutQueryFlow: Flow = allUpdatesFlow.filterIsInstance() - val pollFlow: Flow = allUpdatesFlow.filterIsInstance() - val pollAnswerFlow: Flow = allUpdatesFlow.filterIsInstance() - val unknownUpdateTypeFlow: Flow = allUpdatesFlow.filterIsInstance() + override val messageFlow: Flow = allUpdatesFlow.filterIsInstance() + override val messageMediaGroupFlow: Flow = allUpdatesFlow.filterIsInstance() + override val editedMessageFlow: Flow = allUpdatesFlow.filterIsInstance() + override val editedMessageMediaGroupFlow: Flow = allUpdatesFlow.filterIsInstance() + override val channelPostFlow: Flow = allUpdatesFlow.filterIsInstance() + override val channelPostMediaGroupFlow: Flow = allUpdatesFlow.filterIsInstance() + override val editedChannelPostFlow: Flow = allUpdatesFlow.filterIsInstance() + override val editedChannelPostMediaGroupFlow: Flow = allUpdatesFlow.filterIsInstance() + override val chosenInlineResultFlow: Flow = allUpdatesFlow.filterIsInstance() + override val inlineQueryFlow: Flow = allUpdatesFlow.filterIsInstance() + override val callbackQueryFlow: Flow = allUpdatesFlow.filterIsInstance() + override val shippingQueryFlow: Flow = allUpdatesFlow.filterIsInstance() + override val preCheckoutQueryFlow: Flow = allUpdatesFlow.filterIsInstance() + override val pollFlow: Flow = allUpdatesFlow.filterIsInstance() + override val pollAnswerFlow: Flow = allUpdatesFlow.filterIsInstance() + override val unknownUpdateTypeFlow: Flow = allUpdatesFlow.filterIsInstance() } \ No newline at end of file diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/CaptionAndTextSourcesToText.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/CaptionAndTextSourcesToText.kt index 143dd51d53..c5347a12ad 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/CaptionAndTextSourcesToText.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/internal/CaptionAndTextSourcesToText.kt @@ -1,9 +1,10 @@ package dev.inmo.tgbotapi.utils.internal import dev.inmo.tgbotapi.CommonAbstracts.* -import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.ParseMode.* +import dev.inmo.tgbotapi.types.captionLength import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.textLength internal fun createFormattedText( entities: TextSourcesList, diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/passport/DecryptionContext.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/passport/DecryptionContext.kt new file mode 100644 index 0000000000..f179e3f627 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/passport/DecryptionContext.kt @@ -0,0 +1,15 @@ +package dev.inmo.tgbotapi.utils.passport + +import dev.inmo.micro_utils.crypto.SourceBytes +import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.requests.DownloadFile +import dev.inmo.tgbotapi.requests.get.GetFile +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedCredentials +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedData +import dev.inmo.tgbotapi.types.passport.encrypted.PassportFile +import dev.inmo.tgbotapi.utils.nonstrictJsonFormat +import kotlinx.serialization.json.JsonObject + +interface Decryptor { + fun decrypt(data: EncryptedData): SourceBytes +} diff --git a/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/types/passport/FileHashes.kt b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/types/passport/FileHashes.kt new file mode 100644 index 0000000000..0249909252 --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/types/passport/FileHashes.kt @@ -0,0 +1,7 @@ +package dev.inmo.tgbotapi.types.passport + +import dev.inmo.micro_utils.crypto.MD5 +import java.io.File + +val File.passportFileHash: MD5 + get() = readBytes().passportFileHash diff --git a/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/AESDecryptor.kt b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/AESDecryptor.kt new file mode 100644 index 0000000000..99b5185bd0 --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/AESDecryptor.kt @@ -0,0 +1,21 @@ +package dev.inmo.tgbotapi.utils.passport + +import dev.inmo.micro_utils.crypto.SourceBytes +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedData +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class AESDecryptor(key: SourceBytes, private val iv: ByteArray) : Decryptor { + private val key = SecretKeySpec(key, "AES"); + + override fun decrypt(data: EncryptedData): SourceBytes { + return Cipher.getInstance("AES/CBC/NOPADDING").run { + init(Cipher.DECRYPT_MODE, key, IvParameterSpec(this@AESDecryptor.iv)) + val decryptedCredentials = doFinal(data) + + val padding = decryptedCredentials.first() + decryptedCredentials.copyOfRange(padding.toInt(), decryptedCredentials.size) + } + } +} diff --git a/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/CredentialsDecrypting.kt b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/CredentialsDecrypting.kt new file mode 100644 index 0000000000..7639ee5845 --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/CredentialsDecrypting.kt @@ -0,0 +1,29 @@ +package dev.inmo.tgbotapi.utils.passport + +import dev.inmo.micro_utils.crypto.decodeBase64 +import dev.inmo.tgbotapi.types.passport.credentials.DecryptedCredentials +import dev.inmo.tgbotapi.types.passport.credentials.EncryptedCredentials +import dev.inmo.tgbotapi.utils.nonstrictJsonFormat +import java.security.* +import java.security.spec.PKCS8EncodedKeySpec +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +private val regexToRemoveFromKey = Regex("(-----(BEGIN|END) ((?:.*? KEY)|CERTIFICATE)-----|[\\s])") + +fun EncryptedCredentials.decryptWithPKCS8PrivateKey(privateKey: PrivateKey): DecryptedCredentials { + val decrypted = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding").run { + init(Cipher.DECRYPT_MODE, privateKey) + doFinal(secret) + } + val dataDecryptor = (decrypted to hash).createDecryptor() + val decryptedCredentials = dataDecryptor.decrypt(data).decodeToString() + return nonstrictJsonFormat.decodeFromString(DecryptedCredentials.serializer(), decryptedCredentials) +} + +fun EncryptedCredentials.decryptWithPKCS8PrivateKey(key: String) = decryptWithPKCS8PrivateKey( + KeyFactory.getInstance("RSA").generatePrivate( + PKCS8EncodedKeySpec(key.replace(regexToRemoveFromKey, "").decodeBase64()) + ) +) diff --git a/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/ElementDecryptingWithSecureData.kt b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/ElementDecryptingWithSecureData.kt new file mode 100644 index 0000000000..e184139b0d --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/ElementDecryptingWithSecureData.kt @@ -0,0 +1,28 @@ +package dev.inmo.tgbotapi.utils.passport + +import dev.inmo.micro_utils.crypto.SourceBytes +import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.requests.DownloadFile +import dev.inmo.tgbotapi.requests.get.GetFile +import dev.inmo.tgbotapi.types.passport.credentials.* +import dev.inmo.tgbotapi.types.passport.encrypted.PassportFile + +fun EndDataCredentials.decryptData( + bytes: EncryptedData +): SourceBytes { + return createDecryptor().decrypt(bytes) +} + +fun FileCredentials.decryptFile( + fileBytes: ByteArray +): SourceBytes { + return createDecryptor().decrypt(fileBytes) +} +suspend fun FileCredentials.decryptFile( + bot: TelegramBot, + passportFile: PassportFile +): SourceBytes { + val pathedFile = bot.execute(GetFile(passportFile.fileId)) + val bytes = bot.execute(DownloadFile(pathedFile.filePath)) + return decryptFile(bytes) +} diff --git a/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/EndDataDecryptor.kt b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/EndDataDecryptor.kt new file mode 100644 index 0000000000..02a509a53e --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/EndDataDecryptor.kt @@ -0,0 +1,15 @@ +package dev.inmo.tgbotapi.utils.passport + +import dev.inmo.micro_utils.crypto.SourceBytes +import dev.inmo.tgbotapi.types.passport.credentials.EndDataCredentials +import java.security.MessageDigest + +fun Pair.createDecryptor(): Decryptor { + val secretHash = MessageDigest.getInstance("SHA-512").digest(first + second) + val key = secretHash.copyOf(32) + val iv = secretHash.copyOfRange(32, 48) + + return AESDecryptor(key, iv) +} + +fun EndDataCredentials.createDecryptor() = (secret to hash).createDecryptor() diff --git a/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/PassportDataDecryptionHandling.kt b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/PassportDataDecryptionHandling.kt new file mode 100644 index 0000000000..a5a7dcfaaf --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/utils/passport/PassportDataDecryptionHandling.kt @@ -0,0 +1,24 @@ +package dev.inmo.tgbotapi.utils.passport + +import dev.inmo.tgbotapi.types.passport.PassportData +import dev.inmo.tgbotapi.types.passport.decrypted.SecureData +import java.security.PrivateKey + +inline fun PassportData.doInDecryptionContextWithPKCS8Key( + pkcs8Key: PrivateKey, + expectedNonce: String? = null, + crossinline block: SecureData.() -> T +): T { + val decryptedCredentials = credentials.decryptWithPKCS8PrivateKey(pkcs8Key) + expectedNonce ?.let { require(expectedNonce == decryptedCredentials.nonce) } + return decryptedCredentials.secureData.run(block) +} +inline fun PassportData.doInDecryptionContextWithPKCS8Key( + pkcs8Key: String, + expectedNonce: String? = null, + crossinline block: SecureData.() -> T +): T { + val decryptedCredentials = credentials.decryptWithPKCS8PrivateKey(pkcs8Key) + expectedNonce ?.let { require(expectedNonce == decryptedCredentials.nonce) } + return decryptedCredentials.secureData.run(block) +} diff --git a/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/InternalUtils/UpdatesUtils.kt b/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/InternalUtils/UpdatesUtils.kt index 42caeeb6b8..8a35681bd7 100644 --- a/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/InternalUtils/UpdatesUtils.kt +++ b/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/InternalUtils/UpdatesUtils.kt @@ -23,7 +23,7 @@ internal fun List.convertWithMediaGroupUpdates(): List { val resultUpdates = mutableListOf() val mediaGroups = mutableMapOf>() for (update in this) { - val data = (update.data as? MediaGroupMessage) + val data = (update.data as? MediaGroupMessage<*>) if (data == null) { resultUpdates.add(update) continue diff --git a/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/passport/SetPassportDataErrors.kt b/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/passport/SetPassportDataErrors.kt new file mode 100644 index 0000000000..5e19524a04 --- /dev/null +++ b/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/passport/SetPassportDataErrors.kt @@ -0,0 +1,45 @@ +package dev.inmo.tgbotapi.extensions.api.passport + +import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.requests.SetPassportDataErrors +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.message.PassportMessage +import dev.inmo.tgbotapi.types.passport.PassportData +import dev.inmo.tgbotapi.types.passport.PassportElementError +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElement +import dev.inmo.tgbotapi.utils.passport.Decryptor + +suspend fun TelegramBot.setPassportDataErrors( + userId: UserId, + errors: List +) = execute(SetPassportDataErrors(userId, errors)) +suspend fun TelegramBot.setPassportDataErrors( + user: User, + errors: List +) = setPassportDataErrors(user.id, errors) + +suspend fun TelegramBot.setPassportDataErrors( + userId: UserId, + passportData: PassportData, + decryptor: Decryptor, + mapper: suspend Decryptor.(EncryptedPassportElement) -> PassportElementError +): Boolean = setPassportDataErrors( + userId, + passportData.data.map { decryptor.mapper(it) }.also { + if (it.isEmpty()) { + return@setPassportDataErrors false + } + } +) +suspend fun TelegramBot.setPassportDataErrors( + user: User, + passportData: PassportData, + decryptor: Decryptor, + mapper: suspend Decryptor.(EncryptedPassportElement) -> PassportElementError +) = setPassportDataErrors(user.id, passportData, decryptor, mapper) + +suspend fun TelegramBot.setPassportDataErrors( + passportMessage: PassportMessage, + decryptor: Decryptor, + mapper: suspend Decryptor.(EncryptedPassportElement) -> PassportElementError +) = setPassportDataErrors(passportMessage.user, passportMessage.passportData, decryptor, mapper) diff --git a/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/media/SendMediaGroup.kt b/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/media/SendMediaGroup.kt index 6a115897b8..e8cf10588f 100644 --- a/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/media/SendMediaGroup.kt +++ b/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/send/media/SendMediaGroup.kt @@ -7,6 +7,8 @@ import dev.inmo.tgbotapi.types.InputMedia.* import dev.inmo.tgbotapi.types.MessageIdentifier import dev.inmo.tgbotapi.types.chat.abstracts.Chat import dev.inmo.tgbotapi.types.message.abstracts.Message +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent +import dev.inmo.tgbotapi.types.message.content.abstracts.VisualMediaGroupContent import dev.inmo.tgbotapi.utils.RiskFeature /** @@ -20,7 +22,7 @@ suspend fun TelegramBot.sendMediaGroup( replyToMessageId: MessageIdentifier? = null, allowSendingWithoutReply: Boolean? = null ) = execute( - SendMediaGroup( + SendMediaGroup( chatId, media, disableNotification, replyToMessageId, allowSendingWithoutReply ) ) diff --git a/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/utils/UpdatesHandling.kt b/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/utils/UpdatesHandling.kt index 3f9e249fea..54845e4c3a 100644 --- a/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/utils/UpdatesHandling.kt +++ b/tgbotapi.extensions.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/utils/UpdatesHandling.kt @@ -31,7 +31,7 @@ fun CoroutineScope.updateHandlerWithMediaGroupsAdaptation( launch { for (update in updatesChannel) { when (val data = update.data) { - is MediaGroupMessage -> mediaGroupChannel.send("${data.mediaGroupId}${update::class.simpleName}" to update as BaseMessageUpdate) + is MediaGroupMessage<*> -> mediaGroupChannel.send("${data.mediaGroupId}${update::class.simpleName}" to update as BaseMessageUpdate) else -> output(update) } } diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContext.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContext.kt index 5d3afb72a1..d881a5f1ba 100644 --- a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContext.kt +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContext.kt @@ -1,9 +1,11 @@ package dev.inmo.tgbotapi.extensions.behaviour_builder +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.types.update.abstracts.Update import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter -import dev.inmo.tgbotapi.updateshandlers.UpdatesFilter -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter typealias BehaviourContextReceiver = suspend BehaviourContext.() -> T typealias BehaviourContextAndTypeReceiver = suspend BehaviourContext.(I) -> T @@ -19,4 +21,48 @@ data class BehaviourContext( val bot: TelegramBot, val scope: CoroutineScope, val flowsUpdatesFilter: FlowsUpdatesFilter = FlowsUpdatesFilter() -) : UpdatesFilter by flowsUpdatesFilter, TelegramBot by bot, CoroutineScope by scope +) : FlowsUpdatesFilter by flowsUpdatesFilter, TelegramBot by bot, CoroutineScope by scope + +/** + * Creates new one [BehaviourContext], adding subsequent [FlowsUpdatesFilter] in case [newFlowsUpdatesFilterSetUp] is provided and + * [CoroutineScope] as new [BehaviourContext.scope] + * + * @param newFlowsUpdatesFilterSetUp As a parameter receives [FlowsUpdatesFilter] from old [this] [BehaviourContext.flowsUpdatesFilter] + */ +suspend fun BehaviourContext.doInSubContextWithFlowsUpdatesFilterSetup( + newFlowsUpdatesFilterSetUp: BehaviourContextAndTypeReceiver?, + behaviourContextReceiver: BehaviourContextReceiver +) = copy( + flowsUpdatesFilter = FlowsUpdatesFilter(), + scope = CoroutineScope(scope.coroutineContext + SupervisorJob()) +).run { + newFlowsUpdatesFilterSetUp ?.let { + it.apply { invoke(this@run, this@doInSubContextWithFlowsUpdatesFilterSetup.flowsUpdatesFilter) } + } + behaviourContextReceiver().also { stop() } +} + +/** + * Creates new one [BehaviourContext], adding subsequent [FlowsUpdatesFilter] in case [updatesFilter] is provided and + * [CoroutineScope] as new [BehaviourContext.scope] + */ +suspend fun BehaviourContext.doInSubContextWithUpdatesFilter( + updatesFilter: BehaviourContextAndTypeReceiver?, + behaviourContextReceiver: BehaviourContextReceiver +) = doInSubContextWithFlowsUpdatesFilterSetup( + newFlowsUpdatesFilterSetUp = updatesFilter ?.let { + { oldOne -> + oldOne.allUpdatesFlow.filter { updatesFilter(it) }.subscribeSafelyWithoutExceptions(scope, asUpdateReceiver) + } + }, + behaviourContextReceiver +) + +suspend fun BehaviourContext.doInSubContext( + behaviourContextReceiver: BehaviourContextReceiver +) = doInSubContextWithFlowsUpdatesFilterSetup(newFlowsUpdatesFilterSetUp = null, behaviourContextReceiver) + +/** + * This method will cancel ALL subsequent contexts, expectations and waiters + */ +fun BehaviourContext.stop() = scope.cancel() diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/Variants.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/Variants.kt new file mode 100644 index 0000000000..63ed115bef --- /dev/null +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/Variants.kt @@ -0,0 +1,31 @@ +package dev.inmo.tgbotapi.extensions.behaviour_builder + +import dev.inmo.micro_utils.coroutines.DeferredAction +import dev.inmo.micro_utils.coroutines.invokeFirstOf +import kotlinx.coroutines.* + +suspend fun BehaviourContext.parallel( + action: BehaviourContextReceiver +) = async { + action() +} + +inline infix fun Deferred.withAction(noinline callback: suspend (T) -> O) = DeferredAction(this, callback) + +inline fun Deferred.asAction() = DeferredAction(this) { it } + +suspend fun BehaviourContext.oneOfActions( + deferredActions: Iterable> +) = deferredActions.invokeFirstOf(scope) + +suspend fun BehaviourContext.oneOfActions( + vararg deferredActions: DeferredAction<*, O> +) = this@oneOfActions.oneOfActions(deferredActions.toList()) + +suspend fun BehaviourContext.oneOf( + deferredActions: Iterable> +) = oneOfActions(deferredActions.map { it.asAction() }) + +suspend fun BehaviourContext.oneOf( + vararg deferredActions: Deferred +) = oneOf(deferredActions.toList()) diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitContent.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitContent.kt index 26a55849df..fe5cdbf153 100644 --- a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitContent.kt +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitContent.kt @@ -59,6 +59,12 @@ private suspend inline fun BehaviourContext.waitCon } } +suspend fun BehaviourContext.waitContentMessage( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null }, + count: Int = 1, + filter: CommonMessageToContentMapper? = null +) = waitContent(count, initRequest, false, errorFactory, filter) suspend fun BehaviourContext.waitContact( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, @@ -101,14 +107,14 @@ suspend fun BehaviourContext.waitVenue( count: Int = 1, filter: CommonMessageToContentMapper? = null ) = waitContent(count, initRequest, false, errorFactory, filter) -suspend fun BehaviourContext.waitAudioMediaGroup( +suspend fun BehaviourContext.waitAudioMediaGroupContent( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, includeMediaGroups: Boolean = true, filter: CommonMessageToContentMapper? = null ) = waitContent(count, initRequest, includeMediaGroups, errorFactory, filter) -suspend fun BehaviourContext.waitDocumentMediaGroup( +suspend fun BehaviourContext.waitDocumentMediaGroupContent( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, @@ -119,17 +125,17 @@ suspend fun BehaviourContext.waitMedia( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, filter: CommonMessageToContentMapper? = null ) = waitContent(count, initRequest, includeMediaGroups, errorFactory, filter) -suspend fun BehaviourContext.waitMediaGroup( +suspend fun BehaviourContext.waitAnyMediaGroupContent( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, includeMediaGroups: Boolean = true, filter: CommonMessageToContentMapper? = null ) = waitContent(count, initRequest, includeMediaGroups, errorFactory, filter) -suspend fun BehaviourContext.waitVisualMediaGroup( +suspend fun BehaviourContext.waitVisualMediaGroupContent( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, @@ -146,21 +152,21 @@ suspend fun BehaviourContext.waitAudio( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, filter: CommonMessageToContentMapper? = null ) = waitContent(count, initRequest, includeMediaGroups, errorFactory, filter) suspend fun BehaviourContext.waitDocument( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, filter: CommonMessageToContentMapper? = null ) = waitContent(count, initRequest, includeMediaGroups, errorFactory, filter) suspend fun BehaviourContext.waitPhoto( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, filter: CommonMessageToContentMapper? = null ) = waitContent(count, initRequest, includeMediaGroups, errorFactory, filter) suspend fun BehaviourContext.waitSticker( @@ -173,7 +179,7 @@ suspend fun BehaviourContext.waitVideo( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, filter: CommonMessageToContentMapper? = null ) = waitContent(count, initRequest, includeMediaGroups, errorFactory, filter) suspend fun BehaviourContext.waitVideoNote( diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitMediaGroup.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitMediaGroup.kt index f94531487a..5aa0bc8e90 100644 --- a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitMediaGroup.kt +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitMediaGroup.kt @@ -12,14 +12,14 @@ import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList @PreviewFeature -internal suspend inline fun BehaviourContext.onMediaGroup( +internal suspend inline fun BehaviourContext.buildMediaGroupWaiter( count: Int = 1, initRequest: Request<*>? = null, noinline errorFactory: NullableRequestBuilder<*> = { null }, - noinline filter: (suspend (List) -> Boolean)? = null + noinline filter: (suspend (List>) -> Boolean)? = null ) = flowsUpdatesFilter.expectFlow(bot, initRequest, count, errorFactory) { update -> update.asSentMediaGroupUpdate() ?.data ?.let { mediaGroup -> - if (mediaGroup.all { message -> message.content is T } && (filter == null || filter(mediaGroup))) { + if (mediaGroup.all { message -> message.content is T } && (filter == null || filter(mediaGroup as List>))) { listOf( mediaGroup.map { it.content as T } ) @@ -29,33 +29,39 @@ internal suspend inline fun BehaviourContext.onM } ?: emptyList() }.take(count).toList() +suspend fun BehaviourContext.waitMediaGroup( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null }, + count: Int = 1, + filter: (suspend (List>) -> Boolean)? = null +) = buildMediaGroupWaiter(count, initRequest, errorFactory, filter) suspend fun BehaviourContext.waitPlaylist( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - filter: (suspend (List) -> Boolean)? = null -) = onMediaGroup(count, initRequest, errorFactory, filter) + filter: (suspend (List>) -> Boolean)? = null +) = buildMediaGroupWaiter(count, initRequest, errorFactory, filter) suspend fun BehaviourContext.waitDocumentsGroup( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - filter: (suspend (List) -> Boolean)? = null -) = onMediaGroup(count, initRequest, errorFactory, filter) + filter: (suspend (List>) -> Boolean)? = null +) = buildMediaGroupWaiter(count, initRequest, errorFactory, filter) suspend fun BehaviourContext.waitVisualGallery( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - filter: (suspend (List) -> Boolean)? = null -) = onMediaGroup(count, initRequest, errorFactory, filter) + filter: (suspend (List>) -> Boolean)? = null +) = buildMediaGroupWaiter(count, initRequest, errorFactory, filter) suspend fun BehaviourContext.waitPhotoGallery( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - filter: (suspend (List) -> Boolean)? = null -) = onMediaGroup(count, initRequest, errorFactory, filter) + filter: (suspend (List>) -> Boolean)? = null +) = buildMediaGroupWaiter(count, initRequest, errorFactory, filter) suspend fun BehaviourContext.waitVideoGallery( initRequest: Request<*>? = null, errorFactory: NullableRequestBuilder<*> = { null }, count: Int = 1, - filter: (suspend (List) -> Boolean)? = null -) = onMediaGroup(count, initRequest, errorFactory, filter) + filter: (suspend (List>) -> Boolean)? = null +) = buildMediaGroupWaiter(count, initRequest, errorFactory, filter) diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitPassportData.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitPassportData.kt new file mode 100644 index 0000000000..309a4705d0 --- /dev/null +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitPassportData.kt @@ -0,0 +1,54 @@ +package dev.inmo.tgbotapi.extensions.behaviour_builder.expectations + +import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.extensions.utils.* +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.types.message.PassportMessage +import dev.inmo.tgbotapi.types.passport.PassportData +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElement +import dev.inmo.tgbotapi.utils.RiskFeature +import kotlinx.coroutines.flow.toList + +typealias PassportMessageMapper = suspend PassportMessage.() -> PassportData + +@RiskFeature("Do not use this message directly, use waitPassportMessagesWith or waitAnyPassportMessages instead") +suspend fun BehaviourContext.waitPassportMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null }, + count: Int = 1, + mapper: suspend PassportMessage.() -> O? +): List = expectFlow( + initRequest, + count, + errorFactory +) { + it.asMessageUpdate() ?.data ?.asPassportMessage() ?.mapper().let(::listOfNotNull) +}.toList().toList() + +suspend inline fun BehaviourContext.waitPassportMessagesWith( + count: Int = 1, + initRequest: Request<*>? = null, + noinline errorFactory: NullableRequestBuilder<*> = { null }, + noinline filter: PassportMessageMapper? = null +) : List = waitPassportMessages( + initRequest, + errorFactory, + count +) { + if (passportData.data.any { it is T }) { + if (filter == null) { + passportData + } else { + filter(this) + } + } else { + null + } +} + +suspend fun BehaviourContext.waitAnyPassportMessages( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null }, + count: Int = 1, + filter: PassportMessageMapper? = null +) = waitPassportMessagesWith(count, initRequest, errorFactory, filter) diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/CallbackQueryTriggers.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/CallbackQueryTriggers.kt index 5432c7fa58..d66c64eab8 100644 --- a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/CallbackQueryTriggers.kt +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/CallbackQueryTriggers.kt @@ -1,18 +1,12 @@ package dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling -import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextAndTypeReceiver +import dev.inmo.tgbotapi.extensions.behaviour_builder.* import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.expectFlow import dev.inmo.tgbotapi.extensions.utils.* import dev.inmo.tgbotapi.extensions.utils.extensions.sourceChat import dev.inmo.tgbotapi.types.CallbackQuery.* -import dev.inmo.tgbotapi.types.message.ChatEvents.* -import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.* -import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter -import kotlinx.coroutines.flow.filter internal suspend inline fun BehaviourContext.onCallbackQuery( includeFilterByChatInBehaviourSubContext: Boolean = true, @@ -27,19 +21,15 @@ internal suspend inline fun BehaviourContext.onCallb } }.let(::listOfNotNull) }.subscribeSafelyWithoutExceptions(scope) { triggerQuery -> - val (jobToCancel, scenario) = if (includeFilterByChatInBehaviourSubContext) { - val subFilter = FlowsUpdatesFilter() - val subBehaviourContext = copy(flowsUpdatesFilter = subFilter) - - flowsUpdatesFilter.allUpdatesFlow.filter { - val chat = it.sourceChat() ?: return@filter false - chat.id.chatId == triggerQuery.user.id.chatId - }.subscribeSafelyWithoutExceptions(scope, subFilter.asUpdateReceiver) to subBehaviourContext - } else { - null to this + doInSubContextWithUpdatesFilter( + updatesFilter = if (includeFilterByChatInBehaviourSubContext) { + { it.sourceChat() ?.id ?.chatId == triggerQuery.user.id.chatId } + } else { + null + } + ) { + scenarioReceiver(triggerQuery) } - safelyWithoutExceptions { scenario.scenarioReceiver(triggerQuery) } - jobToCancel ?.cancel() } diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/CommandHandling.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/CommandHandling.kt index ab7ebe9060..35c88e1f0e 100644 --- a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/CommandHandling.kt +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/CommandHandling.kt @@ -26,6 +26,12 @@ suspend fun BehaviourContext.command( }, scenarioReceiver ) +suspend fun BehaviourContext.command( + command: String, + requireOnlyCommandInMessage: Boolean = true, + includeFilterByChatInBehaviourSubContext: Boolean = true, + scenarioReceiver: BehaviourContextAndTypeReceiver> +) = command(command.toRegex(), requireOnlyCommandInMessage, includeFilterByChatInBehaviourSubContext, scenarioReceiver) suspend inline fun BehaviourContext.onCommand( commandRegex: Regex, @@ -33,3 +39,10 @@ suspend inline fun BehaviourContext.onCommand( includeFilterByChatInBehaviourSubContext: Boolean = true, noinline scenarioReceiver: BehaviourContextAndTypeReceiver> ): Job = command(commandRegex, requireOnlyCommandInMessage, includeFilterByChatInBehaviourSubContext, scenarioReceiver) + +suspend inline fun BehaviourContext.onCommand( + command: String, + requireOnlyCommandInMessage: Boolean = true, + includeFilterByChatInBehaviourSubContext: Boolean = true, + noinline scenarioReceiver: BehaviourContextAndTypeReceiver> +): Job = onCommand(command.toRegex(), requireOnlyCommandInMessage, includeFilterByChatInBehaviourSubContext, scenarioReceiver) diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/ContentTriggers.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/ContentTriggers.kt index a12e414286..591686026e 100644 --- a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/ContentTriggers.kt +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/ContentTriggers.kt @@ -2,11 +2,8 @@ package dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling - -import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextAndTypeReceiver +import dev.inmo.tgbotapi.extensions.behaviour_builder.* import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.expectFlow import dev.inmo.tgbotapi.extensions.utils.* import dev.inmo.tgbotapi.extensions.utils.extensions.sourceChat @@ -16,9 +13,7 @@ import dev.inmo.tgbotapi.types.message.content.* import dev.inmo.tgbotapi.types.message.content.abstracts.* import dev.inmo.tgbotapi.types.message.content.media.* import dev.inmo.tgbotapi.types.message.payments.InvoiceContent -import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter import dev.inmo.tgbotapi.utils.PreviewFeature -import kotlinx.coroutines.flow.filter typealias CommonMessageFilter = (suspend (CommonMessage) -> Boolean) @@ -50,21 +45,22 @@ internal suspend inline fun BehaviourContext.onCont } }.let(::listOfNotNull) }.subscribeSafelyWithoutExceptions(scope) { triggerMessage -> - val (jobToCancel, scenario) = if (includeFilterByChatInBehaviourSubContext) { - val subFilter = FlowsUpdatesFilter() - val subBehaviourContext = copy(flowsUpdatesFilter = subFilter) - - flowsUpdatesFilter.allUpdatesFlow.filter { - val chat = it.sourceChat() ?: return@filter false - chat.id.chatId == triggerMessage.chat.id.chatId - }.subscribeSafelyWithoutExceptions(scope, subFilter.asUpdateReceiver) to subBehaviourContext - } else { - null to this + doInSubContextWithUpdatesFilter( + updatesFilter = if (includeFilterByChatInBehaviourSubContext) { + { it.sourceChat() ?.id ?.chatId == triggerMessage.chat.id.chatId } + } else { + null + } + ) { + scenarioReceiver(triggerMessage) } - safelyWithoutExceptions { scenario.scenarioReceiver(triggerMessage) } - jobToCancel ?.cancel() } +suspend fun BehaviourContext.onContentMessage( + includeFilterByChatInBehaviourSubContext: Boolean = true, + additionalFilter: CommonMessageFilter? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver> +) = onContent(includeFilterByChatInBehaviourSubContext, false, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onContact( includeFilterByChatInBehaviourSubContext: Boolean = true, additionalFilter: CommonMessageFilter? = null, @@ -105,7 +101,7 @@ suspend fun BehaviourContext.onAudioMediaGroup( additionalFilter: CommonMessageFilter? = null, scenarioReceiver: BehaviourContextAndTypeReceiver> ) = onContent(includeFilterByChatInBehaviourSubContext, true, additionalFilter, scenarioReceiver) -suspend fun BehaviourContext.onDocumentMediaGroup( +suspend fun BehaviourContext.onDocumentMediaGroupContent( includeFilterByChatInBehaviourSubContext: Boolean = true, includeMediaGroups: Boolean = true, additionalFilter: CommonMessageFilter? = null, @@ -113,7 +109,7 @@ suspend fun BehaviourContext.onDocumentMediaGroup( ) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onMediaCollection( includeFilterByChatInBehaviourSubContext: Boolean = true, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, additionalFilter: (suspend (CommonMessage>) -> Boolean)? = null, scenarioReceiver: BehaviourContextAndTypeReceiver>> ) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) @@ -123,18 +119,6 @@ suspend fun BehaviourContext.onMedia( additionalFilter: CommonMessageFilter? = null, scenarioReceiver: BehaviourContextAndTypeReceiver> ) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) -suspend fun BehaviourContext.onMediaGroup( - includeFilterByChatInBehaviourSubContext: Boolean = true, - includeMediaGroups: Boolean = true, - additionalFilter: CommonMessageFilter? = null, - scenarioReceiver: BehaviourContextAndTypeReceiver> -) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) -suspend fun BehaviourContext.onVisualMediaGroup( - includeFilterByChatInBehaviourSubContext: Boolean = true, - includeMediaGroups: Boolean = true, - additionalFilter: CommonMessageFilter? = null, - scenarioReceiver: BehaviourContextAndTypeReceiver> -) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onAnimation( includeFilterByChatInBehaviourSubContext: Boolean = true, additionalFilter: CommonMessageFilter? = null, @@ -142,19 +126,19 @@ suspend fun BehaviourContext.onAnimation( ) = onContent(includeFilterByChatInBehaviourSubContext, false, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onAudio( includeFilterByChatInBehaviourSubContext: Boolean = true, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, additionalFilter: CommonMessageFilter? = null, scenarioReceiver: BehaviourContextAndTypeReceiver> ) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onDocument( includeFilterByChatInBehaviourSubContext: Boolean = true, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, additionalFilter: CommonMessageFilter? = null, scenarioReceiver: BehaviourContextAndTypeReceiver> ) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onPhoto( includeFilterByChatInBehaviourSubContext: Boolean = true, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, additionalFilter: CommonMessageFilter? = null, scenarioReceiver: BehaviourContextAndTypeReceiver> ) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) @@ -165,7 +149,7 @@ suspend fun BehaviourContext.onSticker( ) = onContent(includeFilterByChatInBehaviourSubContext, false, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onVideo( includeFilterByChatInBehaviourSubContext: Boolean = true, - includeMediaGroups: Boolean = true, + includeMediaGroups: Boolean = false, additionalFilter: CommonMessageFilter? = null, scenarioReceiver: BehaviourContextAndTypeReceiver> ) = onContent(includeFilterByChatInBehaviourSubContext, includeMediaGroups, additionalFilter, scenarioReceiver) diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/EventTriggers.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/EventTriggers.kt index c3a9d1b0da..d5b9404550 100644 --- a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/EventTriggers.kt +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/EventTriggers.kt @@ -1,21 +1,14 @@ package dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling -import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextAndTypeReceiver +import dev.inmo.tgbotapi.extensions.behaviour_builder.* import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.expectFlow import dev.inmo.tgbotapi.extensions.utils.* import dev.inmo.tgbotapi.extensions.utils.extensions.sourceChat import dev.inmo.tgbotapi.types.message.ChatEvents.* import dev.inmo.tgbotapi.types.message.ChatEvents.abstracts.* import dev.inmo.tgbotapi.types.message.abstracts.ChatEventMessage -import dev.inmo.tgbotapi.types.message.content.* -import dev.inmo.tgbotapi.types.message.content.abstracts.* -import dev.inmo.tgbotapi.types.message.content.media.* -import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter -import kotlinx.coroutines.flow.filter internal suspend inline fun BehaviourContext.onEvent( includeFilterByChatInBehaviourSubContext: Boolean = true, @@ -31,19 +24,13 @@ internal suspend inline fun BehaviourContext.onEvent( } }.let(::listOfNotNull) }.subscribeSafelyWithoutExceptions(scope) { triggerMessage -> - val (jobToCancel, scenario) = if (includeFilterByChatInBehaviourSubContext) { - val subFilter = FlowsUpdatesFilter() - val subBehaviourContext = copy(flowsUpdatesFilter = subFilter) - - flowsUpdatesFilter.allUpdatesFlow.filter { - val chat = it.sourceChat() ?: return@filter false - chat.id.chatId == triggerMessage.chat.id.chatId - }.subscribeSafelyWithoutExceptions(scope, subFilter.asUpdateReceiver) to subBehaviourContext - } else { - null to this + doInSubContextWithUpdatesFilter( + updatesFilter = if (includeFilterByChatInBehaviourSubContext) { + { it.sourceChat() ?.id ?.chatId == triggerMessage.chat.id.chatId } + } else null + ) { + scenarioReceiver(triggerMessage) } - safelyWithoutExceptions { scenario.scenarioReceiver(triggerMessage) } - jobToCancel ?.cancel() } suspend fun BehaviourContext.onChannelEvent( diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/MediaGroupTriggers.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/MediaGroupTriggers.kt index e273335bf8..639f2cbeff 100644 --- a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/MediaGroupTriggers.kt +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/MediaGroupTriggers.kt @@ -2,74 +2,74 @@ package dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling -import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextAndTypeReceiver +import dev.inmo.tgbotapi.extensions.behaviour_builder.* import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.expectFlow import dev.inmo.tgbotapi.extensions.utils.* import dev.inmo.tgbotapi.extensions.utils.extensions.sourceChat import dev.inmo.tgbotapi.extensions.utils.shortcuts.chat import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage import dev.inmo.tgbotapi.types.message.content.abstracts.* -import dev.inmo.tgbotapi.types.message.content.media.PhotoContent -import dev.inmo.tgbotapi.types.message.content.media.VideoContent -import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter +import dev.inmo.tgbotapi.types.message.content.media.* import dev.inmo.tgbotapi.utils.PreviewFeature -import kotlinx.coroutines.flow.filter @PreviewFeature -internal suspend inline fun BehaviourContext.onMediaGroup( +internal suspend inline fun BehaviourContext.buildMediaGroupTrigger( includeFilterByChatInBehaviourSubContext: Boolean = true, - noinline additionalFilter: (suspend (List) -> Boolean)? = null, - noinline scenarioReceiver: BehaviourContextAndTypeReceiver> + noinline additionalFilter: (suspend (List>) -> Boolean)? = null, + noinline scenarioReceiver: BehaviourContextAndTypeReceiver>> ) = flowsUpdatesFilter.expectFlow(bot) { update -> update.asSentMediaGroupUpdate() ?.data ?.let { mediaGroup -> - if (mediaGroup.all { message -> message.content is T } && (additionalFilter == null || additionalFilter(mediaGroup))) { - listOf(mediaGroup) + if (mediaGroup.all { message -> message.content is T } && (additionalFilter == null || additionalFilter(mediaGroup as List>))) { + listOf(mediaGroup as List>) } else { null } } ?: emptyList() }.subscribeSafelyWithoutExceptions(scope) { mediaGroup -> - val (jobToCancel, scenario) = if (includeFilterByChatInBehaviourSubContext) { - val subFilter = FlowsUpdatesFilter() - val subBehaviourContext = copy(flowsUpdatesFilter = subFilter) - - flowsUpdatesFilter.allUpdatesFlow.filter { - val chat = it.sourceChat() ?: return@filter false - chat.id.chatId == mediaGroup.chat!!.id.chatId - }.subscribeSafelyWithoutExceptions(scope, subFilter.asUpdateReceiver) to subBehaviourContext - } else { - null to this + val mediaGroupChat = mediaGroup.chat!! + doInSubContextWithUpdatesFilter( + updatesFilter = if (includeFilterByChatInBehaviourSubContext) { + { it.sourceChat() ?.id ?.chatId == mediaGroupChat.id.chatId } + } else null + ) { + scenarioReceiver(mediaGroup) } - safelyWithoutExceptions { scenario.scenarioReceiver(mediaGroup) } - jobToCancel ?.cancel() } +suspend fun BehaviourContext.onMediaGroup( + includeFilterByChatInBehaviourSubContext: Boolean = true, + additionalFilter: (suspend (List>) -> Boolean)? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver>> +) = buildMediaGroupTrigger(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onPlaylist( includeFilterByChatInBehaviourSubContext: Boolean = true, - additionalFilter: (suspend (List) -> Boolean)? = null, - scenarioReceiver: BehaviourContextAndTypeReceiver> -) = onMediaGroup(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) + additionalFilter: (suspend (List>) -> Boolean)? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver>> +) = buildMediaGroupTrigger(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onDocumentsGroup( includeFilterByChatInBehaviourSubContext: Boolean = true, - additionalFilter: (suspend (List) -> Boolean)? = null, - scenarioReceiver: BehaviourContextAndTypeReceiver> -) = onMediaGroup(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) + additionalFilter: (suspend (List>) -> Boolean)? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver>> +) = buildMediaGroupTrigger(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onVisualGallery( includeFilterByChatInBehaviourSubContext: Boolean = true, - additionalFilter: (suspend (List) -> Boolean)? = null, - scenarioReceiver: BehaviourContextAndTypeReceiver> -) = onMediaGroup(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) + additionalFilter: (suspend (List>) -> Boolean)? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver>> +) = buildMediaGroupTrigger(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) +suspend fun BehaviourContext.onVisualMediaGroup( + includeFilterByChatInBehaviourSubContext: Boolean = true, + additionalFilter: (suspend (List>) -> Boolean)? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver>> +) = onVisualGallery(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onPhotoGallery( includeFilterByChatInBehaviourSubContext: Boolean = true, - additionalFilter: (suspend (List) -> Boolean)? = null, - scenarioReceiver: BehaviourContextAndTypeReceiver> -) = onMediaGroup(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) + additionalFilter: (suspend (List>) -> Boolean)? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver>> +) = buildMediaGroupTrigger(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) suspend fun BehaviourContext.onVideoGallery( includeFilterByChatInBehaviourSubContext: Boolean = true, - additionalFilter: (suspend (List) -> Boolean)? = null, - scenarioReceiver: BehaviourContextAndTypeReceiver> -) = onMediaGroup(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) + additionalFilter: (suspend (List>) -> Boolean)? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver>> +) = buildMediaGroupTrigger(includeFilterByChatInBehaviourSubContext, additionalFilter, scenarioReceiver) diff --git a/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/PassportTriggers.kt b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/PassportTriggers.kt new file mode 100644 index 0000000000..971a55419f --- /dev/null +++ b/tgbotapi.extensions.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/PassportTriggers.kt @@ -0,0 +1,42 @@ +package dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling + +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.tgbotapi.extensions.behaviour_builder.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.expectFlow +import dev.inmo.tgbotapi.extensions.utils.* +import dev.inmo.tgbotapi.extensions.utils.extensions.sourceChat +import dev.inmo.tgbotapi.types.message.PassportMessage +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.EncryptedPassportElement + +suspend inline fun BehaviourContext.onPassportMessageWith( + includeFilterByChatInBehaviourSubContext: Boolean = true, + noinline additionalFilter: (suspend (PassportMessage) -> Boolean)? = null, + noinline scenarioReceiver: BehaviourContextAndTypeReceiver +) = flowsUpdatesFilter.expectFlow(bot) { + it.asMessageUpdate() ?.data ?.asPassportMessage() ?.let { message -> + if (message.passportData.data.any { it is T }) { + if (additionalFilter == null || additionalFilter(message)) message else null + } else { + null + } + }.let(::listOfNotNull) +}.subscribeSafelyWithoutExceptions(scope) { triggerMessage -> + doInSubContextWithUpdatesFilter( + updatesFilter = if (includeFilterByChatInBehaviourSubContext) { + { it.sourceChat() ?.id ?.chatId == triggerMessage.chat.id.chatId } + } else null + ) { + scenarioReceiver(triggerMessage) + } +} + +suspend fun BehaviourContext.onPassportMessage( + includeFilterByChatInBehaviourSubContext: Boolean = true, + additionalFilter: (suspend (PassportMessage) -> Boolean)? = null, + scenarioReceiver: BehaviourContextAndTypeReceiver +) = onPassportMessageWith( + includeFilterByChatInBehaviourSubContext, + additionalFilter, + scenarioReceiver +) + diff --git a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCasts.kt b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCasts.kt index a6956c3282..b9ec7bb441 100644 --- a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCasts.kt +++ b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCasts.kt @@ -37,6 +37,11 @@ import dev.inmo.tgbotapi.types.message.content.* import dev.inmo.tgbotapi.types.message.content.abstracts.* import dev.inmo.tgbotapi.types.message.content.media.* import dev.inmo.tgbotapi.types.message.payments.InvoiceContent +import dev.inmo.tgbotapi.types.passport.* +import dev.inmo.tgbotapi.types.passport.decrypted.* +import dev.inmo.tgbotapi.types.passport.decrypted.abstracts.* +import dev.inmo.tgbotapi.types.passport.encrypted.* +import dev.inmo.tgbotapi.types.passport.encrypted.abstracts.* import dev.inmo.tgbotapi.types.polls.* import dev.inmo.tgbotapi.types.update.* import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.* @@ -156,21 +161,273 @@ inline fun CallbackQuery.asUnknownCallbackQueryType(): UnknownCallbackQueryType? @PreviewFeature inline fun CallbackQuery.requireUnknownCallbackQueryType(): UnknownCallbackQueryType = this as UnknownCallbackQueryType @PreviewFeature +inline fun PassportElementError.asPassportElementErrorDataField(): PassportElementErrorDataField? = this as? PassportElementErrorDataField +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorDataField(): PassportElementErrorDataField = this as PassportElementErrorDataField +@PreviewFeature +inline fun PassportElementError.asPassportElementErrorFile(): PassportElementErrorFile? = this as? PassportElementErrorFile +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorFile(): PassportElementErrorFile = this as PassportElementErrorFile +@PreviewFeature +inline fun PassportElementError.asPassportElementErrorFiles(): PassportElementErrorFiles? = this as? PassportElementErrorFiles +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorFiles(): PassportElementErrorFiles = this as PassportElementErrorFiles +@PreviewFeature +inline fun PassportElementError.asPassportElementErrorFrontSide(): PassportElementErrorFrontSide? = this as? PassportElementErrorFrontSide +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorFrontSide(): PassportElementErrorFrontSide = this as PassportElementErrorFrontSide +@PreviewFeature +inline fun PassportElementError.asPassportElementErrorReverseSide(): PassportElementErrorReverseSide? = this as? PassportElementErrorReverseSide +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorReverseSide(): PassportElementErrorReverseSide = this as PassportElementErrorReverseSide +@PreviewFeature +inline fun PassportElementError.asPassportElementErrorSelfie(): PassportElementErrorSelfie? = this as? PassportElementErrorSelfie +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorSelfie(): PassportElementErrorSelfie = this as PassportElementErrorSelfie +@PreviewFeature +inline fun PassportElementError.asPassportElementErrorTranslationFile(): PassportElementErrorTranslationFile? = this as? PassportElementErrorTranslationFile +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorTranslationFile(): PassportElementErrorTranslationFile = this as PassportElementErrorTranslationFile +@PreviewFeature +inline fun PassportElementError.asPassportElementErrorTranslationFiles(): PassportElementErrorTranslationFiles? = this as? PassportElementErrorTranslationFiles +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorTranslationFiles(): PassportElementErrorTranslationFiles = this as PassportElementErrorTranslationFiles +@PreviewFeature +inline fun PassportElementError.asPassportElementErrorUnspecified(): PassportElementErrorUnspecified? = this as? PassportElementErrorUnspecified +@PreviewFeature +inline fun PassportElementError.requirePassportElementErrorUnspecified(): PassportElementErrorUnspecified = this as PassportElementErrorUnspecified +@PreviewFeature +inline fun PassportElementError.asPassportElementFileError(): PassportElementFileError? = this as? PassportElementFileError +@PreviewFeature +inline fun PassportElementError.requirePassportElementFileError(): PassportElementFileError = this as PassportElementFileError +@PreviewFeature +inline fun PassportElementError.asPassportElementFilesError(): PassportElementFilesError? = this as? PassportElementFilesError +@PreviewFeature +inline fun PassportElementError.requirePassportElementFilesError(): PassportElementFilesError = this as PassportElementFilesError +@PreviewFeature +inline fun PassportElementError.asPassportMultipleElementsError(): PassportMultipleElementsError? = this as? PassportMultipleElementsError +@PreviewFeature +inline fun PassportElementError.requirePassportMultipleElementsError(): PassportMultipleElementsError = this as PassportMultipleElementsError +@PreviewFeature +inline fun PassportElementError.asPassportSingleElementError(): PassportSingleElementError? = this as? PassportSingleElementError +@PreviewFeature +inline fun PassportElementError.requirePassportSingleElementError(): PassportSingleElementError = this as PassportSingleElementError +@PreviewFeature +inline fun PassportElementError.asUnknownPassportElementError(): UnknownPassportElementError? = this as? UnknownPassportElementError +@PreviewFeature +inline fun PassportElementError.requireUnknownPassportElementError(): UnknownPassportElementError = this as UnknownPassportElementError +@PreviewFeature +inline fun EncryptedPassportElement.asBankStatement(): BankStatement? = this as? BankStatement +@PreviewFeature +inline fun EncryptedPassportElement.requireBankStatement(): BankStatement = this as BankStatement +@PreviewFeature +inline fun EncryptedPassportElement.asCommonPassport(): CommonPassport? = this as? CommonPassport +@PreviewFeature +inline fun EncryptedPassportElement.requireCommonPassport(): CommonPassport = this as CommonPassport +@PreviewFeature +inline fun EncryptedPassportElement.asDriverLicense(): DriverLicense? = this as? DriverLicense +@PreviewFeature +inline fun EncryptedPassportElement.requireDriverLicense(): DriverLicense = this as DriverLicense +@PreviewFeature +inline fun EncryptedPassportElement.asEmail(): Email? = this as? Email +@PreviewFeature +inline fun EncryptedPassportElement.requireEmail(): Email = this as Email +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedAddress(): EncryptedAddress? = this as? EncryptedAddress +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedAddress(): EncryptedAddress = this as EncryptedAddress +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPersonalDetails(): EncryptedPersonalDetails? = this as? EncryptedPersonalDetails +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPersonalDetails(): EncryptedPersonalDetails = this as EncryptedPersonalDetails +@PreviewFeature +inline fun EncryptedPassportElement.asIdentityCard(): IdentityCard? = this as? IdentityCard +@PreviewFeature +inline fun EncryptedPassportElement.requireIdentityCard(): IdentityCard = this as IdentityCard +@PreviewFeature +inline fun EncryptedPassportElement.asInternalPassport(): InternalPassport? = this as? InternalPassport +@PreviewFeature +inline fun EncryptedPassportElement.requireInternalPassport(): InternalPassport = this as InternalPassport +@PreviewFeature +inline fun EncryptedPassportElement.asPassport(): Passport? = this as? Passport +@PreviewFeature +inline fun EncryptedPassportElement.requirePassport(): Passport = this as Passport +@PreviewFeature +inline fun EncryptedPassportElement.asPassportRegistration(): PassportRegistration? = this as? PassportRegistration +@PreviewFeature +inline fun EncryptedPassportElement.requirePassportRegistration(): PassportRegistration = this as PassportRegistration +@PreviewFeature +inline fun EncryptedPassportElement.asPhoneNumber(): PhoneNumber? = this as? PhoneNumber +@PreviewFeature +inline fun EncryptedPassportElement.requirePhoneNumber(): PhoneNumber = this as PhoneNumber +@PreviewFeature +inline fun EncryptedPassportElement.asRentalAgreement(): RentalAgreement? = this as? RentalAgreement +@PreviewFeature +inline fun EncryptedPassportElement.requireRentalAgreement(): RentalAgreement = this as RentalAgreement +@PreviewFeature +inline fun EncryptedPassportElement.asTemporaryRegistration(): TemporaryRegistration? = this as? TemporaryRegistration +@PreviewFeature +inline fun EncryptedPassportElement.requireTemporaryRegistration(): TemporaryRegistration = this as TemporaryRegistration +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithTranslatableFilesCollection(): EncryptedPassportElementWithTranslatableFilesCollection? = this as? EncryptedPassportElementWithTranslatableFilesCollection +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithTranslatableFilesCollection(): EncryptedPassportElementWithTranslatableFilesCollection = this as EncryptedPassportElementWithTranslatableFilesCollection +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithTranslatableIDDocument(): EncryptedPassportElementWithTranslatableIDDocument? = this as? EncryptedPassportElementWithTranslatableIDDocument +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithTranslatableIDDocument(): EncryptedPassportElementWithTranslatableIDDocument = this as EncryptedPassportElementWithTranslatableIDDocument +@PreviewFeature +inline fun EncryptedPassportElement.asUtilityBill(): UtilityBill? = this as? UtilityBill +@PreviewFeature +inline fun EncryptedPassportElement.requireUtilityBill(): UtilityBill = this as UtilityBill +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithFilesCollection(): EncryptedPassportElementWithFilesCollection? = this as? EncryptedPassportElementWithFilesCollection +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithFilesCollection(): EncryptedPassportElementWithFilesCollection = this as EncryptedPassportElementWithFilesCollection +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementTranslatable(): EncryptedPassportElementTranslatable? = this as? EncryptedPassportElementTranslatable +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementTranslatable(): EncryptedPassportElementTranslatable = this as EncryptedPassportElementTranslatable +@PreviewFeature +inline fun EncryptedPassportElement.asUnknownEncryptedPassportElement(): UnknownEncryptedPassportElement? = this as? UnknownEncryptedPassportElement +@PreviewFeature +inline fun EncryptedPassportElement.requireUnknownEncryptedPassportElement(): UnknownEncryptedPassportElement = this as UnknownEncryptedPassportElement +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithData(): EncryptedPassportElementWithData? = this as? EncryptedPassportElementWithData +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithData(): EncryptedPassportElementWithData = this as EncryptedPassportElementWithData +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithEmail(): EncryptedPassportElementWithEmail? = this as? EncryptedPassportElementWithEmail +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithEmail(): EncryptedPassportElementWithEmail = this as EncryptedPassportElementWithEmail +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithFrontSide(): EncryptedPassportElementWithFrontSide? = this as? EncryptedPassportElementWithFrontSide +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithFrontSide(): EncryptedPassportElementWithFrontSide = this as EncryptedPassportElementWithFrontSide +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithPhoneNumber(): EncryptedPassportElementWithPhoneNumber? = this as? EncryptedPassportElementWithPhoneNumber +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithPhoneNumber(): EncryptedPassportElementWithPhoneNumber = this as EncryptedPassportElementWithPhoneNumber +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithReverseSide(): EncryptedPassportElementWithReverseSide? = this as? EncryptedPassportElementWithReverseSide +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithReverseSide(): EncryptedPassportElementWithReverseSide = this as EncryptedPassportElementWithReverseSide +@PreviewFeature +inline fun EncryptedPassportElement.asEncryptedPassportElementWithSelfie(): EncryptedPassportElementWithSelfie? = this as? EncryptedPassportElementWithSelfie +@PreviewFeature +inline fun EncryptedPassportElement.requireEncryptedPassportElementWithSelfie(): EncryptedPassportElementWithSelfie = this as EncryptedPassportElementWithSelfie +@PreviewFeature +inline fun SecureValue.asAddressSecureValue(): AddressSecureValue? = this as? AddressSecureValue +@PreviewFeature +inline fun SecureValue.requireAddressSecureValue(): AddressSecureValue = this as AddressSecureValue +@PreviewFeature +inline fun SecureValue.asBankStatementSecureValue(): BankStatementSecureValue? = this as? BankStatementSecureValue +@PreviewFeature +inline fun SecureValue.requireBankStatementSecureValue(): BankStatementSecureValue = this as BankStatementSecureValue +@PreviewFeature +inline fun SecureValue.asCommonPassportSecureValue(): CommonPassportSecureValue? = this as? CommonPassportSecureValue +@PreviewFeature +inline fun SecureValue.requireCommonPassportSecureValue(): CommonPassportSecureValue = this as CommonPassportSecureValue +@PreviewFeature +inline fun SecureValue.asDriverLicenseSecureValue(): DriverLicenseSecureValue? = this as? DriverLicenseSecureValue +@PreviewFeature +inline fun SecureValue.requireDriverLicenseSecureValue(): DriverLicenseSecureValue = this as DriverLicenseSecureValue +@PreviewFeature +inline fun SecureValue.asIdentityCardSecureValue(): IdentityCardSecureValue? = this as? IdentityCardSecureValue +@PreviewFeature +inline fun SecureValue.requireIdentityCardSecureValue(): IdentityCardSecureValue = this as IdentityCardSecureValue +@PreviewFeature +inline fun SecureValue.asIdentityWithReverseSideSecureValue(): IdentityWithReverseSideSecureValue? = this as? IdentityWithReverseSideSecureValue +@PreviewFeature +inline fun SecureValue.requireIdentityWithReverseSideSecureValue(): IdentityWithReverseSideSecureValue = this as IdentityWithReverseSideSecureValue +@PreviewFeature +inline fun SecureValue.asInternalPassportSecureValue(): InternalPassportSecureValue? = this as? InternalPassportSecureValue +@PreviewFeature +inline fun SecureValue.requireInternalPassportSecureValue(): InternalPassportSecureValue = this as InternalPassportSecureValue +@PreviewFeature +inline fun SecureValue.asOtherDocumentsSecureValue(): OtherDocumentsSecureValue? = this as? OtherDocumentsSecureValue +@PreviewFeature +inline fun SecureValue.requireOtherDocumentsSecureValue(): OtherDocumentsSecureValue = this as OtherDocumentsSecureValue +@PreviewFeature +inline fun SecureValue.asPassportRegistrationSecureValue(): PassportRegistrationSecureValue? = this as? PassportRegistrationSecureValue +@PreviewFeature +inline fun SecureValue.requirePassportRegistrationSecureValue(): PassportRegistrationSecureValue = this as PassportRegistrationSecureValue +@PreviewFeature +inline fun SecureValue.asPassportSecureValue(): PassportSecureValue? = this as? PassportSecureValue +@PreviewFeature +inline fun SecureValue.requirePassportSecureValue(): PassportSecureValue = this as PassportSecureValue +@PreviewFeature +inline fun SecureValue.asPersonalDetailsSecureValue(): PersonalDetailsSecureValue? = this as? PersonalDetailsSecureValue +@PreviewFeature +inline fun SecureValue.requirePersonalDetailsSecureValue(): PersonalDetailsSecureValue = this as PersonalDetailsSecureValue +@PreviewFeature +inline fun SecureValue.asRentalAgreementSecureValue(): RentalAgreementSecureValue? = this as? RentalAgreementSecureValue +@PreviewFeature +inline fun SecureValue.requireRentalAgreementSecureValue(): RentalAgreementSecureValue = this as RentalAgreementSecureValue +@PreviewFeature +inline fun SecureValue.asTemporalRegistrationSecureValue(): TemporalRegistrationSecureValue? = this as? TemporalRegistrationSecureValue +@PreviewFeature +inline fun SecureValue.requireTemporalRegistrationSecureValue(): TemporalRegistrationSecureValue = this as TemporalRegistrationSecureValue +@PreviewFeature +inline fun SecureValue.asUtilityBillSecureValue(): UtilityBillSecureValue? = this as? UtilityBillSecureValue +@PreviewFeature +inline fun SecureValue.requireUtilityBillSecureValue(): UtilityBillSecureValue = this as UtilityBillSecureValue +@PreviewFeature +inline fun SecureValue.asSecureValueIdentity(): SecureValueIdentity? = this as? SecureValueIdentity +@PreviewFeature +inline fun SecureValue.requireSecureValueIdentity(): SecureValueIdentity = this as SecureValueIdentity +@PreviewFeature +inline fun SecureValue.asSecureValueWithData(): SecureValueWithData? = this as? SecureValueWithData +@PreviewFeature +inline fun SecureValue.requireSecureValueWithData(): SecureValueWithData = this as SecureValueWithData +@PreviewFeature +inline fun SecureValue.asSecureValueWithFiles(): SecureValueWithFiles? = this as? SecureValueWithFiles +@PreviewFeature +inline fun SecureValue.requireSecureValueWithFiles(): SecureValueWithFiles = this as SecureValueWithFiles +@PreviewFeature +inline fun SecureValue.asSecureValueWithReverseSide(): SecureValueWithReverseSide? = this as? SecureValueWithReverseSide +@PreviewFeature +inline fun SecureValue.requireSecureValueWithReverseSide(): SecureValueWithReverseSide = this as SecureValueWithReverseSide +@PreviewFeature +inline fun SecureValue.asSecureValueWithTranslations(): SecureValueWithTranslations? = this as? SecureValueWithTranslations +@PreviewFeature +inline fun SecureValue.requireSecureValueWithTranslations(): SecureValueWithTranslations = this as SecureValueWithTranslations +@PreviewFeature +inline fun Message.asAnonymousGroupMessageImpl(): AnonymousGroupMessageImpl? = this as? AnonymousGroupMessageImpl +@PreviewFeature +inline fun Message.requireAnonymousGroupMessageImpl(): AnonymousGroupMessageImpl = this as AnonymousGroupMessageImpl +@PreviewFeature +inline fun Message.asChannelMessageImpl(): ChannelMessageImpl? = this as? ChannelMessageImpl +@PreviewFeature +inline fun Message.requireChannelMessageImpl(): ChannelMessageImpl = this as ChannelMessageImpl +@PreviewFeature +inline fun Message.asFromChannelGroupMessageImpl(): FromChannelGroupMessageImpl? = this as? FromChannelGroupMessageImpl +@PreviewFeature +inline fun Message.requireFromChannelGroupMessageImpl(): FromChannelGroupMessageImpl = this as FromChannelGroupMessageImpl +@PreviewFeature +inline fun Message.asPassportMessage(): PassportMessage? = this as? PassportMessage +@PreviewFeature +inline fun Message.requirePassportMessage(): PassportMessage = this as PassportMessage +@PreviewFeature +inline fun Message.asPrivateMessageImpl(): PrivateMessageImpl? = this as? PrivateMessageImpl +@PreviewFeature +inline fun Message.requirePrivateMessageImpl(): PrivateMessageImpl = this as PrivateMessageImpl +@PreviewFeature inline fun Message.asChannelEventMessage(): ChannelEventMessage? = this as? ChannelEventMessage @PreviewFeature inline fun Message.requireChannelEventMessage(): ChannelEventMessage = this as ChannelEventMessage @PreviewFeature -inline fun Message.asChannelMediaGroupMessage(): ChannelMediaGroupMessage? = this as? ChannelMediaGroupMessage +inline fun Message.asChannelMediaGroupMessage(): ChannelMediaGroupMessage? = this as? ChannelMediaGroupMessage @PreviewFeature -inline fun Message.requireChannelMediaGroupMessage(): ChannelMediaGroupMessage = this as ChannelMediaGroupMessage +inline fun Message.requireChannelMediaGroupMessage(): ChannelMediaGroupMessage = this as ChannelMediaGroupMessage @PreviewFeature inline fun Message.asCommonGroupEventMessage(): CommonGroupEventMessage? = this as? CommonGroupEventMessage @PreviewFeature inline fun Message.requireCommonGroupEventMessage(): CommonGroupEventMessage = this as CommonGroupEventMessage @PreviewFeature -inline fun Message.asCommonMediaGroupMessage(): CommonMediaGroupMessage? = this as? CommonMediaGroupMessage +inline fun Message.asCommonMediaGroupMessage(): CommonMediaGroupMessage? = this as? CommonMediaGroupMessage @PreviewFeature -inline fun Message.requireCommonMediaGroupMessage(): CommonMediaGroupMessage = this as CommonMediaGroupMessage +inline fun Message.requireCommonMediaGroupMessage(): CommonMediaGroupMessage = this as CommonMediaGroupMessage @PreviewFeature inline fun Message.asCommonSupergroupEventMessage(): CommonSupergroupEventMessage? = this as? CommonSupergroupEventMessage @PreviewFeature @@ -212,9 +469,9 @@ inline fun Message.asGroupMessage(): GroupMessage? = this as? Gr @PreviewFeature inline fun Message.requireGroupMessage(): GroupMessage = this as GroupMessage @PreviewFeature -inline fun Message.asMediaGroupMessage(): MediaGroupMessage? = this as? MediaGroupMessage +inline fun Message.asMediaGroupMessage(): MediaGroupMessage? = this as? MediaGroupMessage @PreviewFeature -inline fun Message.requireMediaGroupMessage(): MediaGroupMessage = this as MediaGroupMessage +inline fun Message.requireMediaGroupMessage(): MediaGroupMessage = this as MediaGroupMessage @PreviewFeature inline fun Message.asPossiblyEditedMessage(): PossiblyEditedMessage? = this as? PossiblyEditedMessage @PreviewFeature diff --git a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/shortcuts/MediaGroupsShortcuts.kt b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/shortcuts/MediaGroupsShortcuts.kt index 031d187697..78376ea9ad 100644 --- a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/shortcuts/MediaGroupsShortcuts.kt +++ b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/shortcuts/MediaGroupsShortcuts.kt @@ -8,13 +8,13 @@ import dev.inmo.tgbotapi.types.message.abstracts.* import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent import dev.inmo.tgbotapi.types.update.MediaGroupUpdates.SentMediaGroupUpdate -val List>.forwardInfo: ForwardInfo? +val List>.forwardInfo: ForwardInfo? get() = firstOrNull() ?.forwardInfo -val List>.replyTo: Message? +val List>.replyTo: Message? get() = firstOrNull() ?.replyTo -val List>.chat: Chat? +val List>.chat: Chat? get() = firstOrNull() ?.chat -val List.mediaGroupId: MediaGroupIdentifier? +val List>.mediaGroupId: MediaGroupIdentifier? get() = firstOrNull() ?.mediaGroupId val SentMediaGroupUpdate.forwardInfo: ForwardInfo? @@ -30,7 +30,7 @@ fun List>.createResend( chatId: ChatId, disableNotification: Boolean = false, replyTo: MessageIdentifier? = null -) = SendMediaGroup( +) = SendMediaGroup( chatId, map { it.content.toMediaGroupMemberInputMedia() }, disableNotification, diff --git a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/SentMessageUpdatesConversations.kt b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/SentMessageUpdatesConversations.kt index 533488ed3e..29897d31aa 100644 --- a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/SentMessageUpdatesConversations.kt +++ b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/SentMessageUpdatesConversations.kt @@ -2,6 +2,7 @@ package dev.inmo.tgbotapi.extensions.utils.updates +import dev.inmo.tgbotapi.types.message.PassportMessage import dev.inmo.tgbotapi.types.message.abstracts.* import dev.inmo.tgbotapi.types.update.abstracts.BaseSentMessageUpdate import kotlinx.coroutines.flow.Flow @@ -26,6 +27,11 @@ inline fun Flow.chatEvents() = mapNotNull { it.data as? ChatEventMessage<*> } +@Suppress("NOTHING_TO_INLINE") +inline fun Flow.passportMessages() = mapNotNull { + it.data as? PassportMessage +} + /** * Will map incoming [BaseSentMessageUpdate]s to [UnknownMessageType] from [BaseSentMessageUpdate.data] */ diff --git a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/UpdatesUtils.kt b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/UpdatesUtils.kt index 4d0963708f..63a3983c87 100644 --- a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/UpdatesUtils.kt +++ b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/UpdatesUtils.kt @@ -34,7 +34,7 @@ fun List.convertWithMediaGroupUpdates(): List { val resultUpdates = mutableListOf() val mediaGroups = mutableMapOf>() for (update in this) { - val data = (update.data as? MediaGroupMessage) + val data = (update.data as? MediaGroupMessage<*>) if (data == null) { resultUpdates.add(update) continue diff --git a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/MediaGroupsIncluder.kt b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/MediaGroupsIncluder.kt index 75ef278a8d..bae30b47ce 100644 --- a/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/MediaGroupsIncluder.kt +++ b/tgbotapi.extensions.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/MediaGroupsIncluder.kt @@ -2,6 +2,7 @@ package dev.inmo.tgbotapi.extensions.utils.updates.retrieving import dev.inmo.tgbotapi.extensions.utils.updates.convertWithMediaGroupUpdates import dev.inmo.tgbotapi.types.message.abstracts.MediaGroupMessage +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaGroupContent import dev.inmo.tgbotapi.types.update.abstracts.BaseMessageUpdate import dev.inmo.tgbotapi.types.update.abstracts.Update import dev.inmo.tgbotapi.updateshandlers.UpdateReceiver @@ -32,7 +33,7 @@ fun CoroutineScope.updateHandlerWithMediaGroupsAdaptation( launch { for (update in updatesChannel) { when (val data = update.data) { - is MediaGroupMessage -> mediaGroupChannel.send("${data.mediaGroupId}${update::class.simpleName}" to update as BaseMessageUpdate) + is MediaGroupMessage<*> -> mediaGroupChannel.send("${data.mediaGroupId}${update::class.simpleName}" to update as BaseMessageUpdate) else -> output(update) } } diff --git a/tgbotapi.extensions.utils/src/jvmMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/Webhook.kt b/tgbotapi.extensions.utils/src/jvmMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/Webhook.kt index fcb780754d..5111c46980 100644 --- a/tgbotapi.extensions.utils/src/jvmMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/Webhook.kt +++ b/tgbotapi.extensions.utils/src/jvmMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/Webhook.kt @@ -34,9 +34,10 @@ import java.util.concurrent.Executors fun Route.includeWebhookHandlingInRoute( scope: CoroutineScope, exceptionsHandler: ExceptionHandler? = null, + mediaGroupsDebounceTimeMillis: Long = 1000L, block: UpdateReceiver ) { - val transformer = scope.updateHandlerWithMediaGroupsAdaptation(block) + val transformer = scope.updateHandlerWithMediaGroupsAdaptation(block, mediaGroupsDebounceTimeMillis) post { safely( exceptionsHandler ?: {} @@ -56,10 +57,12 @@ fun Route.includeWebhookHandlingInRoute( fun Route.includeWebhookHandlingInRouteWithFlows( scope: CoroutineScope, exceptionsHandler: ExceptionHandler? = null, + mediaGroupsDebounceTimeMillis: Long = 1000L, block: FlowsUpdatesFilter.() -> Unit ) = includeWebhookHandlingInRoute( scope, exceptionsHandler, + mediaGroupsDebounceTimeMillis, flowsUpdatesFilter(block = block).asUpdateReceiver ) @@ -83,6 +86,7 @@ fun startListenWebhooks( listenRoute: String? = null, privateKeyConfig: WebhookPrivateKeyConfig? = null, scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + mediaGroupsDebounceTimeMillis: Long = 1000L, block: UpdateReceiver ): ApplicationEngine { val env = applicationEngineEnvironment { @@ -90,8 +94,8 @@ fun startListenWebhooks( module { routing { listenRoute ?.also { - createRouteFromPath(it).includeWebhookHandlingInRoute(scope, exceptionsHandler, block) - } ?: includeWebhookHandlingInRoute(scope, exceptionsHandler, block) + createRouteFromPath(it).includeWebhookHandlingInRoute(scope, exceptionsHandler, mediaGroupsDebounceTimeMillis, block) + } ?: includeWebhookHandlingInRoute(scope, exceptionsHandler, mediaGroupsDebounceTimeMillis, block) } } privateKeyConfig ?.let { @@ -137,10 +141,11 @@ suspend fun RequestsExecutor.setWebhookInfoAndStartListenWebhooks( listenRoute: String = "/", privateKeyConfig: WebhookPrivateKeyConfig? = null, scope: CoroutineScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher()), + mediaGroupsDebounceTimeMillis: Long = 1000L, block: UpdateReceiver ): ApplicationEngine = try { execute(setWebhookRequest) - startListenWebhooks(listenPort, engineFactory, exceptionsHandler, listenHost, listenRoute, privateKeyConfig, scope, block) + startListenWebhooks(listenPort, engineFactory, exceptionsHandler, listenHost, listenRoute, privateKeyConfig, scope, mediaGroupsDebounceTimeMillis, block) } catch (e: Exception) { throw e }